82

I want to watch for changes in a UIView's frame, bounds or center property. How can I use Key-Value Observing to achieve this?

hfossli
  • 22,616
  • 10
  • 116
  • 130
  • 3
    This isn't actually a question. – extremeboredom Feb 02 '11 at 15:28
  • 8
    i just wanted to post my answer to the solution since i couldn't find the solution by googling and stackoverflowing :-) *...sigh!...* so much for sharing... – hfossli Feb 03 '11 at 12:35
  • 12
    it is perfectly fine to ask questions that you find interesting, and that you already have solutions for. However - put more effort into phrasing the question so that it really sounds like a question one would ask. – Bozho Mar 08 '11 at 18:41
  • 2
    Fantastic QA, thanks hfossil !!! – Fattie Sep 16 '14 at 07:57

9 Answers9

74

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");
}];
hfossli
  • 22,616
  • 10
  • 116
  • 130
  • If a view's frame *was* KVO compliant, it *would* be sufficient to observe just the frame. Other properties that influence the frame would trigger a change notification on frame as well (it would be a dependent key). But, as I said, all of that just is not the case and might work only by accident. – Nikolai Ruhe Oct 31 '13 at 09:06
  • 4
    Well CALayer.h says "CALayer implements the standard NSKeyValueCoding protocol for all Objective C properties defined by the class and its subclasses..." So there you go. :) They're observable. – hfossli Oct 31 '13 at 11:30
  • 2
    You are right that Core Animation objects are [documented](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreAnimation_guide/Key-ValueCodingExtensions/Key-ValueCodingExtensions.html#//apple_ref/doc/uid/TP40004514-CH12-SW2) to be KVC compliant. This says nothing about KVO compliance, though. KVC and KVO are just different things (even though KVC compliance is a prerequisite for KVO compliance). – Nikolai Ruhe Oct 31 '13 at 12:29
  • @NikolaiRuhe Okay, makes sense. Anyway in my example on gist.github everything seemed to work well for all cases I've tested so far. – hfossli Oct 31 '13 at 14:46
  • 3
    I'm downvoting to draw attention to the problems attached with your approach of using KVO. I've tried to explain that a working example does not support a recommendation of how to do something properly in code. In case you're not convinced here's another reference to the fact that [it's not possible to observe arbitrary UIKit properties](http://stackoverflow.com/a/6612561/104790). – Nikolai Ruhe Nov 11 '13 at 12:33
  • @NikolaiRuhe Great :D Thanks! I totally agree, but I still want to provide this information even though it might not be fit for production. That has to be a decission each developer has to make on its own. – hfossli Nov 11 '13 at 16:23
  • 5
    Please pass a valid context pointer. Doing so allows you to distinguish between your observations and those of some other object. Not doing so can result in undefined behavior, particular on removal of an observer. – quellish Sep 16 '14 at 08:09
63

EDIT: I don't think this solution is thorough enough. This answer is kept for historical reasons. See my newest answer here: https://stackoverflow.com/a/19687115/202451


You've got to do KVO on the frame-property. "self" is in thise case a UIViewController.

adding the observer (typically done in viewDidLoad):

[self addObserver:self forKeyPath:@"view.frame" options:NSKeyValueObservingOptionOld context:NULL];

removing the observer (typically done in dealloc or viewDidDisappear:):

[self removeObserver:self forKeyPath:@"view.frame"];

Getting information about the change

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if([keyPath isEqualToString:@"view.frame"]) {
        CGRect oldFrame = CGRectNull;
        CGRect newFrame = CGRectNull;
        if([change objectForKey:@"old"] != [NSNull null]) {
            oldFrame = [[change objectForKey:@"old"] CGRectValue];
        }
        if([object valueForKeyPath:keyPath] != [NSNull null]) {
            newFrame = [[object valueForKeyPath:keyPath] CGRectValue];
        }
    }
}

 
Community
  • 1
  • 1
hfossli
  • 22,616
  • 10
  • 116
  • 130
  • Doesn't work. You can add observers for most of the properties in UIView, but not for frame. I get a compiler warning about a "possibly undefined key path 'frame'". Ignoring this warning and doing it anyway, the observeValueForKeyPath method never gets called. – n13 Feb 11 '12 at 16:27
  • Well, works for me. I've now posted an later and more robust version here as well. – hfossli Mar 02 '12 at 16:40
  • 3
    Confirmed, works for me as well. UIView.frame is properly observable. Funny enough, UIView.bounds is not. – Till Oct 16 '12 at 14:31
  • Works for me too, but it doesn't tell me much. stack trace only shows UIApplicationMain and NSKeyValueNotifyObserver.. How do I know what caused the frame change? – eugene Dec 13 '12 at 06:13
  • You can not know what caused it. All you will know is that it changed. – hfossli Dec 15 '12 at 18:01
  • Although not specified in the documentation for NSKeyValueObserving, I think it's probably good practice to call [super] in the observeValueForKeyPath method, just in case any parent class has registered any observers; they will all come through this one method. – Richard Jun 25 '13 at 15:11
  • @Richard have you tested this? I'm not sure if super actually implements this. Last I checked (two years ago iOS 5) I think it didn't :) – hfossli Jun 25 '13 at 15:16
  • 1
    @hfossli You're right that you can't just blindly call [super] - that will throw an exception along the lines of '... message was received but not handled', which is a bit of a shame - you have to actually know that the superclass implements the method before calling it. – Richard Jun 25 '13 at 15:33
  • @Richard I agree! That is a shame. – hfossli Jun 25 '13 at 19:28
  • 3
    -1: Neither `UIViewController` declares `view` nor does `UIView` declare `frame` to be KVO compliant keys. Cocoa and Cocoa-touch do not allow for arbitrary observing of keys. All observable keys have to be properly documented. The fact that it seems to work does not makle this a valid (production safe) way to observe frame changes on a view. – Nikolai Ruhe Oct 29 '13 at 17:34
  • @NikolaiRuhe I totally agree. – hfossli Oct 30 '13 at 14:50
  • @NikolaiRuhe I've provided a new answer to this question. I hope you like it :) – hfossli Oct 30 '13 at 15:33
8

Currently it's not possible to use KVO to observe a view's frame. Properties have to be KVO compliant to be observable. Sadly, properties of the UIKit framework are generally not observable, as with any other system framework.

From the documentation:

Note: Although the classes of the UIKit framework generally do not support KVO, you can still implement it in the custom objects of your application, including custom views.

There are a few exceptions to this rule, like NSOperationQueue's operations property but they have to be explicitly documented.

Even if using KVO on a view's properties might currently work I would not recommend to use it in shipping code. It's a fragile approach and relies on undocumented behavior.

Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • I agree with you about KVO on the property "frame" on UIView. The other answer I provided seems to work just perfectly. – hfossli Oct 31 '13 at 08:39
  • @hfossli ReactiveCocoa is built on KVO. It has the same limitations and problems. It's not a proper way to observe a view's frame. – Nikolai Ruhe Oct 31 '13 at 08:57
  • Yeah, I know that. That's why I wrote you could do ordinary KVO. Using ReactiveCocoa was just for Keeping It Simple. – hfossli Oct 31 '13 at 11:37
  • 1
    Hi @NikolaiRuhe -- it just occurred to me. If Apple can't KVO the frame, how the heck do they implement http://stackoverflow.com/a/25727788/294884 modern Constraints for views?! – Fattie Sep 21 '14 at 07:34
  • @JoeBlow Apple does not have to use KVO. They control the implementation of all of `UIView` so they can use whatever mechanism they see fit. – Nikolai Ruhe Sep 21 '14 at 18:02
  • Hi Nikolai ... indeed, I guess that's correct. it's funny to think that you can't use kvo for the most obvious use of it in the whole ui :) – Fattie Sep 22 '14 at 04:53
4

If I might contribute to the conversation: as others have pointed out, frame is not guaranteed to be key-value observable itself and neither are the CALayer properties even though they appear to be.

What you can do instead is create a custom UIView subclass that overrides setFrame: and announces that receipt to a delegate. Set the autoresizingMask so that the view has flexible everything. Configure it to be entirely transparent and small (to save costs on the CALayer backing, not that it matters a lot) and add it as a subview of the view you want to watch size changes on.

This worked successfully for me way back under iOS 4 when we were first specifying iOS 5 as the API to code to and, as a result, needed a temporary emulation of viewDidLayoutSubviews (albeit that overriding layoutSubviews was more appropriate, but you get the point).

Tommy
  • 99,986
  • 12
  • 185
  • 204
  • You would need to subclass the layer as well to get `transform` etc – hfossli May 26 '14 at 14:15
  • This is a nice, re-usable (give your UIView subclass an init method that takes the view to build constraints against and the viewcontroller to report changes back to and it's easy to deploy anywhere you need it) solution that still works (found overriding setBounds: most effective in my case). Especially handy when you can't use the viewDidLayoutSubviews: approach due to needing to relayout elements. – bcl Mar 27 '19 at 17:15
0

As mentioned, if KVO doesn't work and you just want to observe your own views which you have control over, you can create a custom view that overrides either setFrame or setBounds. A caveat is that the final, desired frame value may not be available at the point of invocation. Thus I added a GCD call to the next main thread loop to check the value again.

-(void)setFrame:(CGRect)frame
{
   NSLog(@"setFrame: %@", NSStringFromCGRect(frame));
   [super setFrame:frame];
   // final value is available in the next main thread cycle
   __weak PositionLabel *ws = self;
   dispatch_async(dispatch_get_main_queue(), ^(void) {
      if (ws && ws.superview)
      {
         NSLog(@"setFrame2: %@", NSStringFromCGRect(ws.frame));
         // do whatever you need to...
      }
   });
}
loungerdork
  • 991
  • 11
  • 15
0

To not rely on KVO observing you could perform method swizzling as follows:

@interface UIView(SetFrameNotification)

extern NSString * const UIViewDidChangeFrameNotification;

@end

@implementation UIView(SetFrameNotification)

#pragma mark - Method swizzling setFrame

static IMP originalSetFrameImp = NULL;
NSString * const UIViewDidChangeFrameNotification = @"UIViewDidChangeFrameNotification";

static void __UIViewSetFrame(id self, SEL _cmd, CGRect frame) {
    ((void(*)(id,SEL, CGRect))originalSetFrameImp)(self, _cmd, frame);
    [[NSNotificationCenter defaultCenter] postNotificationName:UIViewDidChangeFrameNotification object:self];
}

+ (void)load {
    [self swizzleSetFrameMethod];
}

+ (void)swizzleSetFrameMethod {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        IMP swizzleImp = (IMP)__UIViewSetFrame;
        Method method = class_getInstanceMethod([UIView class],
                @selector(setFrame:));
        originalSetFrameImp = method_setImplementation(method, swizzleImp);
    });
}

@end

Now to observe frame change for a UIView in your application code:

- (void)observeFrameChangeForView:(UIView *)view {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewDidChangeFrameNotification:) name:UIViewDidChangeFrameNotification object:view];
}

- (void)viewDidChangeFrameNotification:(NSNotification *)notification {
    UIView *v = (UIView *)notification.object;
    NSLog(@"View '%@' did change frame to %@", v, NSStringFromCGRect(v.frame));
}
Werner Altewischer
  • 10,080
  • 4
  • 53
  • 60
  • Except that you would need to not only swizzle setFrame, but also layer.bounds, layer.transform, layer.position, layer.zPosition, layer.anchorPoint, layer.anchorPointZ and layer.frame. What's wrong with KVO? :) – hfossli Sep 06 '16 at 18:43
0

Updated @hfossli answer for RxSwift and Swift 5.

With RxSwift you can do

Observable.of(rx.observe(CGRect.self, #keyPath(UIView.frame)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.bounds)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.transform)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.position)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.zPosition)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.anchorPoint)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.anchorPointZ)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.frame))
        ).merge().subscribe(onNext: { _ in
                 print("View probably changed its geometry")
            }).disposed(by: rx.disposeBag)

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

Observable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.bounds))).subscribe(onNext: { _ in
                print("View bounds changed its geometry")
            }).disposed(by: rx.disposeBag)

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

Observable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.frame)),
              rx.observe(CGRect.self, #keyPath(UIView.frame))).merge().subscribe(onNext: { _ in
                 print("View frame changed its geometry")
            }).disposed(by: rx.disposeBag)
black_pearl
  • 2,549
  • 1
  • 23
  • 36
-1

There is a way to achieve this without using KVO at all, and for the sake of others finding this post, I'll add it here.

http://www.objc.io/issue-12/animating-custom-layer-properties.html

This excellent tutorial by Nick Lockwood describes how to use core animations timing functions to drive anything. It's far superior to using a timer or CADisplay layer, because you can use the built in timing functions, or fairly easily create your own cubic bezier function (see the accompanying article (http://www.objc.io/issue-12/animations-explained.html) .

Sam Clewlow
  • 4,293
  • 26
  • 36
  • "There is a way to achieve this without using KVO". What is "this" in this context? Can you be a little more specific. – hfossli Nov 04 '14 at 13:20
  • The OP asked for a way to get specific values for a view whilst it's animating. They also asked if it's possible to KVO these properties, which it is, but it's not technically supported. I've suggested checking out the article, which provides a robust solution to the problem. – Sam Clewlow Nov 04 '14 at 15:50
  • Can you be more specific? What part of the article did you find relevant? – hfossli Nov 03 '15 at 08:23
  • @hfossli Looking at it, I think I might have posed this answer to the wrong question, as I can see any mention of animations! Sorry! – Sam Clewlow Nov 03 '15 at 08:49
-4

It's not safe to use KVO in some UIKit properties like frame. Or at least that's what Apple says.

I would recommend using ReactiveCocoa, this will help you listen to changes in any property without using KVO, it's very easy to start observing something using Signals:

[RACObserve(self, frame) subscribeNext:^(CGRect frame) {
    //do whatever you want with the new frame
}];
saky
  • 135
  • 1
  • 6
  • 4
    however, @NikolaiRuhe says "ReactiveCocoa is built on KVO. It has the same limitations and problems. It's not a proper way to observe a view's frame" – Fattie Sep 21 '14 at 07:32