Iphone 如何在UIView';上进行键值观察并获取KVO回调;什么样的框架?

Iphone 如何在UIView';上进行键值观察并获取KVO回调;什么样的框架?,iphone,ios,objective-c,uiview,key-value-observing,Iphone,Ios,Objective C,Uiview,Key Value Observing,我想查看ui视图的帧、边界或中心属性中的更改。我如何使用键值观察来实现这一点?编辑:我认为这个解决方案不够彻底。这个答案是出于历史原因而保留的。请参见我的最新答案: 您必须对frame属性执行KVO。在这种情况下,“self”是UIViewController 添加观察者(通常在viewDidLoad中完成): 删除观察者(通常在dealloc或VIEWDIDGEARE:中完成): 获取有关更改的信息 - (void)observeValueForKeyPath:(NSString *)key

我想查看
ui视图
边界
中心
属性中的更改。我如何使用键值观察来实现这一点?

编辑:我认为这个解决方案不够彻底。这个答案是出于历史原因而保留的。请参见我的最新答案


您必须对frame属性执行KVO。在这种情况下,“self”是UIViewController

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

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

获取有关更改的信息

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

 

通常存在不支持KVO的通知或其他可观察事件。即使医生说“不”,观察支持UIView的CALayer表面上是安全的。由于广泛使用KVO和适当的访问器(而不是ivar操作),观察CALayer在实践中起作用。这并不能保证继续下去

无论如何,视图的框架只是其他属性的产物。因此,我们需要遵守以下规定:

[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];
请参见此处的完整示例

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

注意:请注意,在回调最终值稳定之前,您将收到多个回调。例如,更改视图或图层的帧将导致图层更改
位置
边界
(按该顺序)


使用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");
}];
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)
Observable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.bounds))).subscribe(onNext: { _ in
                print("View bounds changed its geometry")
            }).disposed(by: rx.disposeBag)
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)
如果你只想知道什么时候
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");
}];
@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");
}];
如果您只想知道何时
frame
发生更改,您可以这样做

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

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

从:

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

此规则有一些例外,如NSOperationQueue的
操作
属性,但它们必须明确记录


即使在视图的属性上使用KVO目前可能有效,我也不建议在发布代码中使用它。这是一种脆弱的方法,依赖于未记录的行为。

如果我可以参与对话的话:正如其他人所指出的,
frame
本身不能保证是可观察的关键值,
CALayer
属性也不能保证是可观察的,即使它们看起来是

相反,您可以创建一个自定义的
UIView
子类,该子类重写
setFrame:
,并向代理宣布该收据。设置
autoresizingMask
,使视图具有灵活的所有内容。将其配置为完全透明且小(以节省
CALayer
备份的成本,但这并不重要),并将其添加为要查看大小变化的视图的子视图


早在我们第一次将iOS 5指定为要编码的API时,在iOS 4下,这就成功了,因此需要临时模拟
viewDidLayoutSubviews
(尽管覆盖
layoutSubviews
更合适,但你明白了).

在某些UIKit属性(如
frame
)中使用KVO是不安全的。至少苹果是这么说的

我建议使用,这将帮助您在不使用KVO的情况下监听任何属性中的更改,使用信号很容易观察到某些内容:

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

如前所述,如果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...
      }
   });
}

有一种方法可以在根本不使用KVO的情况下实现这一点,为了其他人找到这篇文章,我将在这里添加它


Nick Lockwood的这篇优秀教程介绍了如何使用核心动画计时功能来驱动任何东西。它远优于使用计时器或CADisplay层,因为您可以使用内置的计时功能,或者相当容易地创建自己的立方贝塞尔函数(请参阅随附文章()。

要不依赖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));
}

更新了@hfossli对RxSwiftSwift 5的回答

使用RxSwift,您可以

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");
}];
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)
Observable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.bounds))).subscribe(onNext: { _ in
                print("View bounds changed its geometry")
            }).disposed(by: rx.disposeBag)
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)
如果你只想知道什么时候
bounds
改变,你可以这样做

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");
}];
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)
Observable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.bounds))).subscribe(onNext: { _ in
                print("View bounds changed its geometry")
            }).disposed(by: rx.disposeBag)
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)
如果您只想知道何时
frame
发生更改,您可以这样做

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");
}];
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)
Observable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.bounds))).subscribe(onNext: { _ in
                print("View bounds changed its geometry")
            }).disposed(by: rx.disposeBag)
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)

这实际上不是一个问题。我只是想发布我的答案,因为我无法通过谷歌搜索和StackOverflow找到解决方案:-)…叹气!。。。分享就这么多…问一些你觉得有趣的问题,并且你已经有了解决方案,这是非常好的。然而-在问题的措辞上投入更多的精力,让它听起来真的像一个人会问的问题。很棒的QA,谢谢hfossil!!!不起作用。可以为UIView中的大多数属性添加观察者,但不能为框架添加观察者。我收到一条关于“可能未定义的键路径‘frame’”的编译器警告。不管怎么做,忽略这个警告,observeValueForKeyPath方法永远不会被调用。我现在在这里发布了一个更晚、更健壮的版本。确认了,对我也适用。UIView.frame可以正确地观察到。有趣的是,UIView.bounds不是。@hfossli你是对的,你不能