属性 UIView 边界和框架的观察者反应不同
Property observers for UIView bounds and frame react differently
当我探索观察 UIView
的 bounds
或 frame
变化(提到 here and )的选项时,我遇到了一个非常奇怪的差异: didSet
和 willSet
将根据您在视图层次结构中放置 UIView
的位置以不同方式触发 :
- 如果我在视图控制器的根目录为
UIView
使用属性观察者,我只会得到didSet
和willSet
来自 frame
的事件发生了变化。
- 如果我对
UIView
使用 属性 观察者,它是视图控制器内的 子视图 ,我只会得到 didSet
和 willSet
来自 bounds
的事件发生了变化。
我想首先说明,我明确避免提到的 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)。 这个问题假定我偏好使用 didSet
和 willSet
来观察 UIView
的 bounds
/ 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)
}
}
}
在我的示例代码中,如果您旋转设备,您会看到在我观察根视图的一种情况下(ViewController
的 self.view
-- 以蓝色显示) ,我永远不会收到 bounds
更改的通知,尽管它实际上已经更改了。子视图则相反——我从未收到 frame
更改的通知,尽管它已经更改。
环境
我正在 Xcode 9.3 上使用 iOS 11.4 SDK 在 iPad Air 和 iPad Pro 等设备上测试这个项目。我还没有试过 iOS 12 beta。
我的问题
- 当
UIView
在视图层次结构中的位置不同时,为什么 didSet
和 willSet
的触发方式不同?
- 当
didSet
for bounds
被触发时,为什么 didSet
for frame
也不会被触发(对于子视图)?反之亦然(对于根视图)?
- 如果有的话,有什么方法可以确保无论我将
UIView
放在视图层次结构中的哪个位置,我都能始终观察到 bounds
的变化?
根据我在 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()
}
}
当我探索观察 UIView
的 bounds
或 frame
变化(提到 here and didSet
和 willSet
将根据您在视图层次结构中放置 UIView
的位置以不同方式触发 :
- 如果我在视图控制器的根目录为
UIView
使用属性观察者,我只会得到didSet
和willSet
来自frame
的事件发生了变化。 - 如果我对
UIView
使用 属性 观察者,它是视图控制器内的 子视图 ,我只会得到didSet
和willSet
来自bounds
的事件发生了变化。
我想首先说明,我明确避免提到的 KVO 方法 here since it’s not officially supported. I’m also not looking to use viewDidLayoutSubviews()
mentioned didSet
和 willSet
来观察 UIView
的 bounds
/ 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)
}
}
}
在我的示例代码中,如果您旋转设备,您会看到在我观察根视图的一种情况下(ViewController
的 self.view
-- 以蓝色显示) ,我永远不会收到 bounds
更改的通知,尽管它实际上已经更改了。子视图则相反——我从未收到 frame
更改的通知,尽管它已经更改。
环境
我正在 Xcode 9.3 上使用 iOS 11.4 SDK 在 iPad Air 和 iPad Pro 等设备上测试这个项目。我还没有试过 iOS 12 beta。
我的问题
- 当
UIView
在视图层次结构中的位置不同时,为什么didSet
和willSet
的触发方式不同? - 当
didSet
forbounds
被触发时,为什么didSet
forframe
也不会被触发(对于子视图)?反之亦然(对于根视图)? - 如果有的话,有什么方法可以确保无论我将
UIView
放在视图层次结构中的哪个位置,我都能始终观察到bounds
的变化?
根据我在 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()
}
}