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

发布于 2024-10-15 05:03:27 字数 117 浏览 2 评论 0原文

我想观察 UIViewframeboundscenter 属性的变化。如何使用键值观察来实现这一目标?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

别理我 2024-10-22 05:03:27

通常存在不支持 KVO 的通知或其他可观察事件。尽管文档说“不”,但观察支持 UIView 的 CALayer 表面上是安全的。观察 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)

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


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

如果你只想知道边界何时改变,你可以做

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

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");
}];
Oo萌小芽oO 2024-10-22 05:03:27

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


你必须对框架属性执行 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];
        }
    }
}

 

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

 
失眠症患者 2024-10-22 05:03:27

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

来自 文档

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

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

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

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.

扮仙女 2024-10-22 05:03:27

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

您可以做的是创建一个自定义 UIView 子类,该子类覆盖 setFrame: 并向委托宣告该收据。设置 autoresizingMask 以便视图具有灵活的一切。将其配置为完全透明且较小(以节省 CALayer 支持的成本,但这并不重要)并将其添加为您想要观看大小变化的视图的子视图。

早在 iOS 4 下,当我们第一次指定 iOS 5 作为编码的 API 时,这对我来说就成功了,因此,需要临时模拟 viewDidLayoutSubviews (尽管重写了 layoutSubviews 更合适,但你明白了)。

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

呢古 2024-10-22 05:03:27

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

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...
      }
   });
}
万人眼中万个我 2024-10-22 05:03:27

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

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));
}
ぶ宁プ宁ぶ 2024-10-22 05:03:27

更新了 RxSwiftSwift 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)

如果您只想知道边界何时发生变化,您可以做到

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)

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)
伴我心暖 2024-10-22 05:03:27

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

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

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

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

梦在夏天 2024-10-22 05:03:27

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

我建议使用 ReactiveCocoa,这将帮助你在不使用 KVO 的情况下监听任何属性的变化,这非常简单使用信号开始观察某些事物:

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

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
}];
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文