UIView是否有大小调整事件?

110
我有一个包含多个图片视图的行和列的视图。
如果这个视图被重新调整大小,我需要重新排列这些图片视图的位置。
这个视图是另一个被调整大小的视图的子视图。
是否有一种方法可以检测到这个视图正在被调整大小?

视图何时调整大小?当设备旋转时,还是当用户使用多点触控旋转时? - Evan Mulawski
当用户在另一个视图(主视图)上点击按钮时,此视图将被调整大小。 - live-love
7个回答

105
如下所述,正确的方法是覆盖layoutSubviews并在那里布置imageViews。
如果由于某种原因无法子类化和覆盖layoutSubviews,观察bounds应该可以工作,即使有点不太好。更糟糕的是,观察存在风险 - 苹果不保证KVO适用于UIKit类。请阅读与苹果工程师的讨论:When does an associated object get released? 原始答案如下:
您可以使用键值观察:
[yourView addObserver:self forKeyPath:@"bounds" options:0 context:nil];

并实现:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == yourView && [keyPath isEqualToString:@"bounds"]) {
        // do your stuff, or better schedule to run later using performSelector:withObject:afterDuration:
    }
}

2
实际上,布置子视图正是 layoutSubviews 的用途,因此观察大小变化虽然可行,但在我看来不是一个好的设计。 - uliwitness
@uliwitness 嗯,他们确实不断地改变这些东西。无论如何,现在你应该使用 viewWillTransition 等等。 - Dan Rosenstark
viewWillTransition是针对UIViewController而非UIView的方法。 - Armand
如果根据当前视图的大小需要添加/删除不同的子视图和约束,那么是否仍建议使用layoutSubviews方法呢? - Chris Prince
1
我认为我刚刚回答了我的情况。当我在override of bounds中进行设置(取决于大小)时,它有效。当我在layoutSubviews中进行设置时,它无效。 - Chris Prince

104
在一个UIView子类中,可以使用属性观察器:
override var bounds: CGRect {
    didSet {
        // ...
    }
}

不需要子类化,使用智能键路径进行键值观察即可:

Without subclassing, key-value observation with smart key-paths will do:

var boundsObservation: NSKeyValueObservation?

func beginObservingBounds() {
    boundsObservation = observe(\.bounds) { capturedSelf, _ in
        // ...
    }
}

2
@Ali 你为什么这样认为? - Rudolf Adamkovič
当加载框架更改时,边界似乎没有改变(我知道这很奇怪,也许是我做错了什么) - Ali
3
正如在这里多次提到的那样,"frame" 是一个派生的、在运行时计算的属性。除非您有一个非常聪明且明确的理由,否则请不要覆盖它。否则,请使用 "bounds" 或者(更好的选择)"layoutSubviews"。 - auco
3
这是一个创建圆形的好方法,非常感谢!override var bounds: CGRect { didSet { layer.cornerRadius = bounds.size.width / 2 }} - SimplGy
5
根据你将视图放置在视图层次结构的位置,检测boundsframe更改无法保证有效。我建议重写layoutSubviews方法。请参考答案。 - HuaTham
显示剩余2条评论

34

创建UIView的子类,并重写layoutSubviews方法


13
问题在于子视图不仅可以更改它们的大小,而且还可以对其大小更改进行动画处理。当UIView运行动画时,并不会每次调用layoutSubviews。 - David Jeske
2
@DavidJeske UIViewAnimationOptions 有一个选项叫做UIViewAnimationOptionLayoutSubviews。我认为这可能有所帮助。:) - Neal.Marlin

9

Swift 4键路径 KVO - 我是如何检测自动旋转并移动到iPad侧面板的。应该适用于任何视图。必须观察UIView的层。

private var observer: NSKeyValueObservation?

override func viewDidLoad() {
    super.viewDidLoad()
    observer = view.layer.observe(\.bounds) { object, _ in
        print(object.bounds)
    }
    // ...
}
override func viewWillDisappear(_ animated: Bool) {
    observer?.invalidate()
    //...
}

这个解决方案对我很有用。我是Swift的新手,不确定你在这里使用的 .bounds 语法是什么意思?它确切地是什么意思? - WBuck
这里有更多讨论:https://github.com/ole/whats-new-in-swift-4/blob/master/Whats-new-in-Swift-4.playground/Pages/Key%20paths.xcplaygroundpage/Contents.swift - Warren Stringer
“.layer” 成功了!您知道为什么使用“view.observe”不起作用吗? - d4Rk
@d4Rk - 我不知道。我猜测图层边界比UIView框架更清晰明了。例如,UITableView的contentOffset会影响其子视图的最终坐标。不确定。 - Warren Stringer
这对我来说是一个很好的答案。我的情况是我正在使用AutoLayout来布置我的视图。但是UICollectionViewFlowLayout强制你提供一个明确的itemSize。但是当你的collectionView大小改变时(就像在我的情况下,当我使用AutoLayout显示工具栏时),itemSize保持不变并抛出=[观察者是迄今为止我得到的最干净的方法。也是最好的! - Yitzchak

8

虽然较老,但仍是一个好问题。在苹果的示例代码和一些私有的UIView子类中,它们大致重写setBounds:

-(void)setBounds:(CGRect)newBounds {
    BOOL const isResize = !CGSizeEqualToSize(newBounds.size, self.bounds.size);
    if (isResize) [self prepareToResizeTo:newBounds.size]; // probably saves 
    [super setBounds:newBounds];
    if (isResize) [self recoverFromResizing];
}

覆盖setFrame:不是一个好主意。 framecenterboundstransform派生,所以iOS不一定会调用setFrame:


2
这在理论上是正确的。然而,在设置框架属性时,setBounds:也不会被调用(至少在iOS 7.1上)。这可能是苹果为避免额外消息而添加的优化。 - nschum
1
这是苹果方面的糟糕设计,没有调用他们自己的访问器。 - osxdirk
2
实际上,framebounds都是从视图的底层CALayer派生出来的;它们只是调用了层的getter方法。而setFrame:设置了层的框架,而setBounds:设置了层的边界。因此,您不能仅覆盖其中一个。另外,layoutSubviews会被过度调用(不仅在几何变化时),因此它也可能不是一个好的选择。还在寻找更好的解决方案... - big_m

7

你可以创建一个UIView的子类并重写setFrame:(CGRect)frame方法。当视图的frame(即大小)发生变化时,该方法会被调用。做如下操作:

- (void) setFrame:(CGRect)frame
{
  // Call the parent class to move the view
  [super setFrame:frame];

  // Do your custom code here.
}

明智而实用。好选择。 - El Zorko
1
FYI,这对UIImageView子类无效。更多信息请参见:https://dev59.com/03fZa4cB1Zd3GeqPUqsD - William Entriken
另外,在我的UITextView子类中,由于自动旋转而导致的调整大小期间,并没有调用“setFrame:”。但是“layoutSubviews:”被调用了。注意:我正在使用自动布局和iOS 7.0。 - ma11hew28
3
永远不要覆盖setFrame:方法,因为frame是一个派生属性。请参考我的回答。 - Adlai Holler

-2

如果您在UIViewController实例中,覆盖viewDidLayoutSubviews方法就可以解决问题。

override func viewDidLayoutSubviews() {
    // update subviews
}

需要更改UIView的大小,而不是UIViewController。 - Gastón Antonio Montes
@GastónAntonioMontes 谢谢你的回复!你可能已经注意到了 UIView 实例和 UIViewController 实例之间的关系。因此,如果你有一个没有 VC 附加的 UIView 实例,其他答案都很好,但如果你恰好附加到一个 VC 上,这就是你需要的。抱歉它不适用于你的情况。 - Dan Rosenstark

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接