如何在UIView的帧上进行键值观察并获得KVO回调?

时间:2011-02-02 12:08:30

标签: iphone ios objective-c uiview key-value-observing

我想留意UIView frameboundscenter媒体资源的变化情况。我如何使用Key-Value Observing来实现这一目标?

9 个答案:

答案 0 :(得分:63)

通常会有KVO不受支持的通知或其他可观察事件。即使文档说'no',观察CALayer支持UIView表面上也是安全的。观察CALayer在实践中是有效的,因为它广泛使用KVO和适当的访问器(而不是ivar操作)。它不能保证继续工作。

无论如何,视图的框架只是其他属性的产物。因此,我们需要观察那些:

[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];

请参阅此处的完整示例 https://gist.github.com/hfossli/7234623

注意:文档中并不支持这一点,但它目前适用于所有iOS版本(目前iOS 2 - > iOS 11)

注意:请注意,在达到最终值之前,您将收到多个回调。例如,更改视图或图层的框架将导致图层更改positionbounds(按此顺序)。


使用ReactiveCocoa,你可以做到

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");
}];

如果您只想知道bounds何时发生了变化

@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");
}];

如果您只想知道frame何时发生了变化

@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");
}];

答案 1 :(得分:61)

编辑:我认为此解决方案不够彻底。这个答案是出于历史原因而保留的。在此处查看我的最新答案https://stackoverflow.com/a/19687115/202451


你必须在frame-property上做KVO。 “self”在这种情况下是一个UIViewController。

添加观察者(通常在viewDidLoad中完成):

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

删除观察者(通常在dealloc或viewDidDisappear中完成:):

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

获取有关更改的信息

- (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];
        }
    }
}

 

答案 2 :(得分:7)

目前无法使用KVO观察视图的帧。属性必须符合 KVO 才能被观察到。遗憾的是,与任何其他系统框架一样,UIKit框架的属性通常是不可观察的。

来自documentation

  

注意:虽然UIKit框架的类通常不支持KVO,但您仍然可以在应用程序的自定义对象中实现它,包括自定义视图。

此规则有一些例外,例如NSOperationQueue的operations属性,但必须明确记录。

即使在视图的属性上使用KVO当前可能有效,我也不建议在运输代码中使用它。这是一种脆弱的方法,依赖于无证件的行为。

答案 3 :(得分:4)

如果我可以为对话做出贡献:正如其他人所指出的那样,frame本身并不保证是键值可观察的,即使它们看起来也不是CALayer属性。< / p>

您可以做的是创建一个自定义UIView子类,它覆盖setFrame:并将该收据通知给代理人。设置autoresizingMask,以便视图具有灵活性。将其配置为完全透明且小(为了节省CALayer支持的成本,而不是重要的事情)并将其添加为您想要观看大小更改的视图的子视图。

当我们第一次将iOS 5指定为要编码的API时,这在iOS 4下成功地运行了,因此需要viewDidLayoutSubviews的临时模拟(虽然覆盖layoutSubviews }更合适,但你明白了。)

答案 4 :(得分:1)

更新了 RxSwift Swift 5 的@hfossli答案。

借助RxSwift,您可以做到

()

如果您只想知道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) 的更改时间,就可以

bounds

如果您只想知道Observable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.bounds))).subscribe(onNext: { _ in print("View bounds changed its geometry") }).disposed(by: rx.disposeBag) 的更改时间,就可以

frame

答案 5 :(得分:0)

如上所述,如果KVO不起作用,您只想观察自己可以控制的视图,则可以创建一个覆盖setFrame或setBounds的自定义视图。需要注意的是,在调用时可能无法获得最终的所需帧值。因此,我向下一个主线程循环添加了一个GCD调用,以再次检查该值。

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

答案 6 :(得分:0)

有一种方法可以在不使用KVO的情况下实现这一目标,并且为了其他人找到这篇文章,我将在此处添加。

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

Nick Lockwood的这篇优秀教程描述了如何使用核心动画计时功能来驱动任何东西。它远远优于使用计时器或CAD显示层,因为您可以使用内置的计时功能,或者相当容易地创建您自己的立方贝塞尔函数(请参阅随附的文章(http://www.objc.io/issue-12/animations-explained.html)。

答案 7 :(得分:0)

为了不依赖KVO观察你可以按如下方式进行方法调整:

@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

现在在应用程序代码中观察UIView的帧更改:

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

答案 8 :(得分:-4)

frame等一些UIKit属性中使用KVO并不安全。或者至少这就是Apple所说的。

我建议使用ReactiveCocoa,这可以帮助您在不使用KVO的情况下聆听任何属性的更改,使用信号启动观察的内容非常容易:

[RACObserve(self, frame) subscribeNext:^(CGRect frame) {
    //do whatever you want with the new frame
}];