问题
While I am exploring the option to observe a UIView
's bounds
or frame
change (mentioned here and here), I have encountered a very strange discrepancy: didSet
and willSet
will be triggered differently based on where you put your UIView
in the view hierarchy:
- If I use property observer for
UIView
at the root of a view controller, I will only getdidSet
andwillSet
events fromframe
changes. - If I use property observer for
UIView
that is a subview inside a view controller, I will only getdidSet
andwillSet
events frombounds
changes.
I’d like to note first that I’m explicitly avoiding KVO approach mentioned here since it’s not officially supported. I’m also not looking to use viewDidLayoutSubviews()
mentioned here since that won’t work for observing changes of subviews (see the doc). This question assumes my preference to use didSet
and willSet
to observe a UIView
’s bounds
/ frame
changes.
The closest question I have come across is this question but it covers only the initialization phrase, and also doesn’t mention the case of observing a subview.
Details
To see this in action, check out 🔨 my sample project.
I am really puzzled why bounds
observers are sometimes not called, so I added frame
observers as well, and even frame
observers are sometimes not called. Eventually, I was able to find the key setting where they work differently: the view’s placement in view hierarchy, as described above.
How I test: in both cases, rotating the device to change the view’s frame
/ bounds
.
Here’s my 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)
}
}
}
In my sample code, if you rotate the device, you will see that in one case where I’m observing the root view (ViewController
’s self.view
-- shown in blue), I would never get notified of bounds
change, despite it having actually changed. The opposite goes for a subview -- I never got notified of frame
change, despite it having changed.
Environment
I'm testing this project on Xcode 9.3 with iOS 11.4 SDK on devices like iPad Air and iPad Pro. I have not tried in on iOS 12 beta.
My Questions
- Why do
didSet
andwillSet
get triggered differently whenUIView
is placed differently in the view hierarchy? - When
didSet
forbounds
is triggered, why won’tdidSet
forframe
triggered as well (for subviews)? Vice versa (for root view)? - What is a way, if any, to ensure that I can always observe
bounds
change of aUIView
no matter where I put it in the view hierarchy?
回答1:
From my repost in Apple Developer Forum, QuinceyMorris
helped me clarifying issues with this approach as well as an approach that would work no matter where I put the view in the view hierarchy.
... 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.
Based on his recommendation that I override layoutSubviews
, here's my updated subclass (just like 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()
}
}
来源:https://stackoverflow.com/questions/50889512/property-observers-for-uiview-bounds-and-frame-react-differently