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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4874288/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 18:45:48  来源:igfitidea点击:

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

iphoneiosobjective-cuiviewkey-value-observing

提问by hfossli

I want to watch for changes in a UIView's frame, boundsor centerproperty. How can I use Key-Value Observing to achieve this?

我想看了在变化UIViewframeboundscenter财产。如何使用键值观察来实现这一点?

采纳答案by hfossli

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.

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

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

回答by hfossli

EDIT: I don't think this solution is thorough enough. This answer is kept for historical reasons. See my newest answerhere: https://stackoverflow.com/a/19687115/202451

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



You've got to do KVO on the frame-property. "self" is in thise case a UIViewController.

你必须在框架属性上做 KVO。在这种情况下,“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];
        }
    }
}

 

回答by Nikolai Ruhe

Currently it's not possible to use KVO to observe a view's frame. Properties have to be KVO compliantto be observable. Sadly, properties of the UIKit framework are generally not observable, as with any other system framework.

目前无法使用 KVO 来观察视图的框架。属性必须符合 KVO才能被观察到。遗憾的是,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 operationsproperty 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. It's a fragile approach and relies on undocumented behavior.

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

回答by Tommy

If I might contribute to the conversation: as others have pointed out, frameis not guaranteed to be key-value observable itself and neither are the CALayerproperties even though they appear to be.

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

What you can do instead is create a custom UIViewsubclass that overrides setFrame:and announces that receipt to a delegate. Set the autoresizingMaskso that the view has flexible everything. Configure it to be entirely transparent and small (to save costs on the CALayerbacking, not that it matters a lot) and add it as a subview of the view you want to watch size changes on.

相反,您可以做的是创建一个自定义UIView子类,该子类覆盖setFrame:并向委托宣布该收据。设置autoresizingMask使视图具有灵活的一切。将其配置为完全透明且很小(以节省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 layoutSubviewswas more appropriate, but you get the point).

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

回答by black_pearl

Updated @hfossli answer for RxSwiftand 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 boundschanges 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 framechanges 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)

回答by Werner Altewischer

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

回答by loungerdork

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.

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

回答by Sam Clewlow

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

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

回答by saky

It's not safe to use KVO in some UIKit properties like frame. Or at least that's what Apple says.

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

I would recommend using ReactiveCocoa, this will help you listen to changes in any property without using KVO, it's very easy to start observingsomething using Signals:

我建议使用ReactiveCocoa,这将帮助您在不使用 KVO 的情况下监听任何属性的变化,使用 Signals开始观察某些内容非常容易:

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