Property Observers for Uiview Bounds and Frame React Differently

Property observers for UIView bounds and frame react differently

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()
}
}

When is bounds and frame set for views/layers in Swift?

Property Observers are not called during initialization.

willSet and didSet observers are not called when a property is first
initialized. They are only called when the property’s value is set
outside of an initialization context.

Perhaps the way the view is being constructed internally, the frame is set post initialization but the bounds are not.

Then later, when setting the frame of the layer, the bounds are also updated by the internal method.

UIView KVO: Why don't changes to center cause KVO notifications for frame?

The main reason observing frame isn't working is because no UIKit property is defined to be KVO-compliant unless explicitly documented to be KVO-compliant.

You're not missing anything - Unfortunately UIKit doesn't support what you're trying to do with the observation.

How can I do Key Value Observing and get a KVO callback on a UIView's frame?

There are usually notifications or other observable events where KVO isn't supported. Even though the docs says 'no', it is ostensibly safe to observe the CALayer backing the UIView. Observing the CALayer works in practice because of its extensive use of KVO and proper accessors (instead of ivar manipulation). It's not guaranteed to work going forward.

Anyway, the view's frame is just the product of other properties. Therefore we need to observe those:

[self.view addObserver:self forKeyPath:@"frame" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"bounds" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"transform" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"position" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"zPosition" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"anchorPoint" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"anchorPointZ" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"frame" options:0 context:NULL];

See full example here
https://gist.github.com/hfossli/7234623

NOTE: This is not said to be supported in the docs, but it works as of today with all iOS versions this far (currently iOS 2 -> iOS 11)

NOTE: Be aware that you will receive multiple callbacks before it settles at its final value. For example changing the frame of a view or layer will cause the layer to change position and bounds (in that order).


With ReactiveCocoa you can do

RACSignal *signal = [RACSignal merge:@[
RACObserve(view, frame),
RACObserve(view, layer.bounds),
RACObserve(view, layer.transform),
RACObserve(view, layer.position),
RACObserve(view, layer.zPosition),
RACObserve(view, layer.anchorPoint),
RACObserve(view, layer.anchorPointZ),
RACObserve(view, layer.frame),
]];

[signal subscribeNext:^(id x) {
NSLog(@"View probably changed its geometry");
}];

And if you only want to know when bounds changes you can do

@weakify(view);
RACSignal *boundsChanged = [[signal map:^id(id value) {
@strongify(view);
return [NSValue valueWithCGRect:view.bounds];
}] distinctUntilChanged];

[boundsChanged subscribeNext:^(id ignore) {
NSLog(@"View bounds changed its geometry");
}];

And if you only want to know when frame changes you can do

@weakify(view);
RACSignal *frameChanged = [[signal map:^id(id value) {
@strongify(view);
return [NSValue valueWithCGRect:view.frame];
}] distinctUntilChanged];

[frameChanged subscribeNext:^(id ignore) {
NSLog(@"View frame changed its geometry");
}];

Observing changes to a UIView's window and superview properties

Here is a way. Is it gross? Yes. Do I recommend such behavior? No. But we're all adults here.

The gist is that you use method_setImplementation to change the implementation of -[UIView didAddSubview:] so you get notified whenever it's called (and you'd do the same thing for willRemoveSubview:). Unfortunately, you will get called for all view hierarchy changes. You'll have to add your own filtering to find the specific views you're interested in.

static void InstallAddSubviewListener(void (^listener)(id _self, UIView* subview))
{
if ( listener == NULL )
{
NSLog(@"listener cannot be NULL.");
return;
}

Method addSubviewMethod = class_getInstanceMethod([UIView class], @selector(didAddSubview:));
IMP originalImp = method_getImplementation(addSubviewMethod);

void (^block)(id, UIView*) = ^(id _self, UIView* subview) {
originalImp(_self, @selector(didAddSubview:), subview);
listener(_self, subview);
};

IMP newImp = imp_implementationWithBlock((__bridge void*)block);
method_setImplementation(addSubviewMethod, newImp);
}

To use, do something like:

InstallAddSubviewListener(^(id _self, UIView *subview) {
NSLog(@"-[UIView didAddSubview:] self=%@, view=%@", _self, subview);
});

Why is it not possible to define property observers for computed propertys?

In the first code, you don't need observers, because you already are writing the code that sets the property (set). Thus, if you want to do something before/after the property gets set, you can just do it right there in the setter handler (set):

class A {
var test : String {
get {
return "foo"
}
set {
// will set
self.test = newValue
// did set
}
}
}

Thus, by a kind of Occam's Razor principle, it would be redundant and unnecessary to have separate setter observers: you are the setter, so there is no need to observe yourself.

In your subclass override, on the other hand, where you didn't supply a whole new computed property, the setting is going on behind your back, as it were, so as compensation you are allowed to inject set observation into the process.

How to detect when UIView size is changed in swift ios xcode?

viewWillLayoutSubviews() and viewDidLayoutSubviews() will be called whenever the bounds change. In the view controller.

Is there a UIView resize event?

As Uli commented below, the proper way to do it is override layoutSubviews and layout the imageViews there.

If, for some reason, you can't subclass and override layoutSubviews, observing bounds should work, even when being kind of dirty. Even worse, there is a risk with observing - Apple does not guarantee KVO works on UIKit classes. Read the discussion with Apple engineer here: When does an associated object get released?

original answer:

You can use key-value observing:

[yourView addObserver:self forKeyPath:@"bounds" options:0 context:nil];

and implement:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == yourView && [keyPath isEqualToString:@"bounds"]) {
// do your stuff, or better schedule to run later using performSelector:withObject:afterDuration:
}
}


Related Topics



Leave a reply



Submit