Property observers for UIView bounds and frame react differently

This is a repost from my SO question here.


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 get `didSet` and `willSet` events from `frame` changes.

- If I use property observer for `UIView` that is a subview inside a view controller, I will only get `didSet` and `willSet` events from `bounds` 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)
        }
    }
}



See some of my screenshots here:

[1]: https://i.stack.imgur.com/xV9EU.png

[2]: https://i.stack.imgur.com/tRiVg.png

[3]: https://i.stack.imgur.com/S8rT5.png


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` and `willSet` get triggered differently when `UIView` is placed differently in the view hierarchy?

- When `didSet` for `bounds` is triggered, why won’t `didSet` for `frame` 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 a `UIView` no matter where I put it in the view hierarchy?

Answered by QuinceyMorris in 317237022

AFAIK, you're headed down a dead end here. It doesn't work the way you expect for a couple of reasons:


— Obj-C properties are behavior, not stored value. This means that they might be backed by a simple instance variable, or their values might be derived from a combination of various internal stored values, or their values might be generated by code, or any combination of these.


The consequence is that 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.


— SInce the bounds and frame property values are related, it's not greatly surprising that a change sometimes goes through one setter and sometimes through the other (as well as sometimes through neither). 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.


— AFAIK, there isn't any direct way of observing bounds changes of an arbitrary view in iOS. On macOS, there are "bounds changed" and "frame changed" notifications that you can opt into, but I'm not aware of anything similar on iOS.


For a non-arbitrary view, such as your BoundsObservableView subclass, the only potential mechanism I know about would be to override layoutSubviews (https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews) and check the current bounds against a stored value. Maybe someone else knows a better way and will jump in with that answer.


The interesting question here is: what are you really trying to do? UIKit has a rich set of features explicitly designed to avoid the need for you to track view sizes yourself. Do you need something that's missing from iOS, or are you just trying to recreate a pattern that you're familiar with from other projects and environments?

There seems to be some logic here, even iof I did not investigate much:


- frame is relative to superview

- bounds is relative to the view itself


hence, when you change bounds, superview does not see it


However, I understand that when you change frame, the view itself should notice.

Accepted Answer

AFAIK, you're headed down a dead end here. It doesn't work the way you expect for a couple of reasons:


— Obj-C properties are behavior, not stored value. This means that they might be backed by a simple instance variable, or their values might be derived from a combination of various internal stored values, or their values might be generated by code, or any combination of these.


The consequence is that 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.


— SInce the bounds and frame property values are related, it's not greatly surprising that a change sometimes goes through one setter and sometimes through the other (as well as sometimes through neither). 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.


— AFAIK, there isn't any direct way of observing bounds changes of an arbitrary view in iOS. On macOS, there are "bounds changed" and "frame changed" notifications that you can opt into, but I'm not aware of anything similar on iOS.


For a non-arbitrary view, such as your BoundsObservableView subclass, the only potential mechanism I know about would be to override layoutSubviews (https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews) and check the current bounds against a stored value. Maybe someone else knows a better way and will jump in with that answer.


The interesting question here is: what are you really trying to do? UIKit has a rich set of features explicitly designed to avoid the need for you to track view sizes yourself. Do you need something that's missing from iOS, or are you just trying to recreate a pattern that you're familiar with from other projects and environments?

Thank you for your reply, QuinceyMorris. I am currently doing a custom form of animation which needs to be updated by `bounds` change. I have found the `layoutSubviews` override approach to be the right solution for me since it works for both cases I mentioned above (view at root or as a subview).

Ok so I have a use case here. I have a UIScrollView as a subview of my custom UIView and I wish to do the following whenever view size is changed:


scrollView.contentInset = UIEdgeInsets(top: self.bounds.height/2, left: 0, bottom: self.bounds.height/2, right: 0)


How do I do it without observing bounds change?

Is this a use case? You have "a UIScrollView as a subview of my custom UIView". Why can't you override layoutSubviews in your custom UIView and adjust the scroll view's contentInset right there?

Ok, so if I understand correctly, layoutSubviews is guaranteed to be called when bounds OR frame changes, correct? If yes, this fulfills my needs. Because I also need to monitor frame changes apart from bounds for other reasons.

Well, you did say "whenever view size is changed", originally. 🙂 The rest is a bit harder.


In iOS, a view has a bounds rect, a center point, and a transform. The frame is a derived property that is only meaningful when the transform is set to the default (identity).


So, yes, layoutSubviews is going to be called when the bounds changes, but not (AFAIK) when the center or transform changes. You can override updateConstraints to monitor whether the center changes because of autolayout.


I think maybe all other positional (center) changes, and all transform changes, can happen only when your code changes them, so you could cover those cases by funnelling changes through a single set of methods.


This isn't what you were looking for, but I don't know that there's a more general solution.

Property observers for UIView bounds and frame react differently
 
 
Q