How can I do Key Value Observing and get a KVO cal

2019-01-02 17:09发布

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?

8条回答
唯独是你
2楼-- · 2019-01-02 17:45

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.

查看更多
无色无味的生活
3楼-- · 2019-01-02 17:54

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));
}
查看更多
泛滥B
4楼-- · 2019-01-02 18:00

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...
      }
   });
}
查看更多
ら面具成の殇う
5楼-- · 2019-01-02 18:03

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");
}];
查看更多
牵手、夕阳
6楼-- · 2019-01-02 18:08

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).

查看更多
无色无味的生活
7楼-- · 2019-01-02 18:09

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) .

查看更多
登录 后发表回答