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
UIViewat the root of a view controller, I will only getdidSetandwillSetevents fromframechanges. - If I use property observer for
UIViewthat is a subview inside a view controller, I will only getdidSetandwillSetevents fromboundschanges.
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
didSetandwillSetget triggered differently whenUIViewis placed differently in the view hierarchy? - When
didSetforboundsis triggered, why won’tdidSetforframetriggered as well (for subviews)? Vice versa (for root view)? - What is a way, if any, to ensure that I can always observe
boundschange of aUIViewno matter where I put it in the view hierarchy?


