属性 UIView 边界和框架的观察者反应不同

Property observers for UIView bounds and frame react differently

当我探索观察 UIViewboundsframe 变化(提到 here and )的选项时,我遇到了一个非常奇怪的差异: didSetwillSet 将根据您在视图层次结构中放置 UIView 的位置以不同方式触发 :

我想首先说明,我明确避免提到的 KVO 方法 here since it’s not officially supported. I’m also not looking to use viewDidLayoutSubviews() mentioned since that won’t work for observing changes of subviews (see the doc)。 这个问题假定我偏好使用 didSetwillSet 来观察 UIViewbounds / frame 变化。

我遇到的最接近的问题是this question,但它只涵盖了初始化短语,也没有提到观察子视图的情况。

详情

要查看实际效果,请查看 my sample project

我真的很纳闷为什么有时候bounds个观察者没有被调用,所以我也加了frame个观察者,甚至frame个观察者有时候也没有被调用。最终,我找到了它们工作方式不同的关键设置:视图在视图层次结构中的位置,如上所述。

我如何测试:在这两种情况下,旋转设备以更改视图的 frame / bounds

这是我的 UIView subclass:

public class BoundsObservableView: UIView {

    public weak var boundsDelegate: ViewBoundsObserving?

    public override var bounds: CGRect {
        willSet {
            print("BOUNDS willSet bounds: \(bounds), frame: \(frame)")
            boundsDelegate?.boundsWillChange(self)
        }
        didSet {
            print("BOUNDS didSet bounds: \(bounds), frame: \(frame)")
            boundsDelegate?.boundsDidChange(self)
        }
    }

    public override var frame: CGRect {
        willSet {
            print("FRAME willSet frame: \(frame), bounds: \(bounds)")
            boundsDelegate?.boundsWillChange(self)
        }
        didSet {
            print("FRAME didSet frame: \(frame), bounds: \(bounds)")
            boundsDelegate?.boundsDidChange(self)
        }
    }
}

在我的示例代码中,如果您旋转设备,您会看到在我观察根视图的一种情况下(ViewControllerself.view -- 以蓝色显示) ,我永远不会收到 bounds 更改的通知,尽管它实际上已经更改了。子视图则相反——我从未收到 frame 更改的通知,尽管它已经更改。

环境

我正在 Xcode 9.3 上使用 iOS 11.4 SDK 在 iPad Air 和 iPad Pro 等设备上测试这个项目。我还没有试过 iOS 12 beta。

我的问题

根据我在 Apple Developer Forum 中的转发,QuinceyMorris 帮助我澄清了这种方法的问题,以及无论我将视图放在视图层次结构中的什么位置都有效的方法。

... an Obj-C property can change value without having its setter called. Changing the instance variable (of simple properties) is a very common Obj-C pattern. It is of course not KVO compliant without additional work, but that's why KVO compliance is not found universally.

... Your willSet/didSet accessors will only trigger when the change goes through their own property. There is nothing you can predict or assume about which property will be used. Even if you see a regularity now, there may be edge cases that are different, and the behavior may change in the future.

根据他的建议,我覆盖了 layoutSubviews,这是我更新的子类(就像 this answer):

public protocol ViewBoundsObserving: class {
    // Notifies the delegate that view's `bounds` has changed.
    // Use `view.bounds` to access current bounds
    func boundsDidChange(_ view: BoundsObservableView, from previousBounds: CGRect);
}

/// You can observe bounds change with this view subclass via `ViewBoundsObserving` delegate.
public class BoundsObservableView: UIView {
    
    public weak var boundsDelegate: ViewBoundsObserving?
    
    private var previousBounds: CGRect = .zero
    
    public override func layoutSubviews() {
        if (bounds != previousBounds) {
            print("Bounds changed from \(previousBounds) to \(bounds)")
            boundsDelegate?.boundsDidChange(self, from: previousBounds)
            previousBounds = bounds
        }
        
        // UIView's implementation will layout subviews for me using Auto Resizing mask or Auto Layout constraints.
        super.layoutSubviews()
    }
}