简体   繁体   English

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

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

I want to watch for changes in a UIView 's frame , bounds or center property.我想观察UIViewframeboundscenter属性的变化。 How can I use Key-Value Observing to achieve this?如何使用键值观察来实现这一点?

There are usually notifications or other observable events where KVO isn't supported.通常有不支持 KVO 的通知或其他可观察事件。 Even though the docs says 'no' , it is ostensibly safe to observe the CALayer backing the UIView.即使文档说'no' ,观察支持 UIView 的 CALayer 表面上是安全的。 Observing the CALayer works in practice because of its extensive use of KVO and proper accessors (instead of ivar manipulation).观察 CALayer 在实践中是有效的,因为它广泛使用了 KVO 和适当的访问器(而不是 ivar 操作)。 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在此处查看完整示例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)注意:文档中不支持这点,但从今天开始,它适用于所有 iOS 版本(目前为 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).例如,更改视图或图层的框架将导致图层更改positionbounds (按该顺序)。


With ReactiveCocoa you can do使用 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");
}];

And if you only want to know when bounds changes you can do如果你只想知道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");
}];

And if you only want to know when frame changes you can do如果您只想知道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");
}];

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在此处查看我的最新答案https : //stackoverflow.com/a/19687115/202451


You've got to do KVO on the frame-property.你必须在框架属性上做 KVO。 "self" is in thise case a UIViewController.在这种情况下,“self”是一个 UIViewController。

adding the observer (typically done in viewDidLoad):添加观察者(通常在 viewDidLoad 中完成):

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

removing the observer (typically done in dealloc or viewDidDisappear:):移除观察者(通常在 dealloc 或 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];
        }
    }
}

 

Currently it's not possible to use KVO to observe a view's frame.目前无法使用 KVO 来观察视图的框架。 Properties have to be KVO compliant to be observable.属性必须符合 KVO才能被观察到。 Sadly, properties of the UIKit framework are generally not observable, as with any other system framework.遗憾的是,UIKit 框架的属性通常是不可观察的,就像任何其他系统框架一样。

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.注意:虽然 UIKit 框架的类一般不支持 KVO,但您仍然可以在您的应用程序的自定义对象中实现它,包括自定义视图。

There are a few exceptions to this rule, like NSOperationQueue's operations property but they have to be explicitly documented.此规则有一些例外,例如 NSOperationQueue 的operations属性,但必须明确记录它们。

Even if using KVO on a view's properties might currently work I would not recommend to use it in shipping code.即使在视图的属性上使用 KVO 目前可能有效,我也不建议在传送代码中使用它。 It's a fragile approach and relies on undocumented behavior.这是一种脆弱的方法,依赖于无证行为。

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

What you can do instead is create a custom UIView subclass that overrides setFrame: and announces that receipt to a delegate.您可以做的是创建一个自定义UIView子类,该子类覆盖setFrame:并将该收据通知给委托。 Set the autoresizingMask so that the view has flexible everything.设置autoresizingMask以便视图具有灵活的一切。 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.将其配置为完全透明且很小(以节省CALayer支持的成本,并不是很重要)并将其添加为您想要观看大小更改的视图的子视图。

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).当我们第一次将 iOS 5 指定为要编码的 API 时,这对我来说在 iOS 4 下很成功,因此,需要临时模拟viewDidLayoutSubviews (尽管覆盖layoutSubviews更合适,但您明白了) .

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.如前所述,如果 KVO 不起作用并且您只想观察您自己控制的视图,您可以创建一个覆盖 setFrame 或 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.因此,我向下一个主线程循环添加了 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...
      }
   });
}

To not rely on KVO observing you could perform method swizzling as follows:为了不依赖 KVO 观察,您可以按如下方式执行方法 swizzling:

@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:现在观察应用程序代码中 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));
}

Updated @hfossli answer for RxSwift and Swift 5 .更新了RxSwiftSwift 5 的@hfossli 答案。

With RxSwift you can do使用 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)

And if you only want to know when bounds changes you can do如果你只想知道bounds何时改变,你可以这样做

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如果您只想知道frame何时更改,您可以这样做

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)

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.有一种方法可以在不使用 KVO 的情况下实现这一点,为了其他人找到这篇文章,我将在此处添加它。

http://www.objc.io/issue-12/animating-custom-layer-properties.html 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. Nick Lockwood 的这个优秀教程描述了如何使用核心动画计时功能来驱动任何东西。 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 ) .它远远优于使用计时器或 CADisplay 层,因为您可以使用内置的计时函数,或者相当轻松地创建自己的三次贝塞尔函数(请参阅随附的文章 ( http://www.objc.io/issue-12/动画-explained.html ) 。

It's not safe to use KVO in some UIKit properties like frame .在某些 UIKit 属性(如frame使用 KVO 是不安全的。 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:我建议使用ReactiveCocoa ,这将帮助您在不使用 KVO 的情况下监听任何属性的变化,使用 Signals 开始观察某些内容非常容易:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM