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


79

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


3
这实际上不是问题。
Extremeboredom 2011年

7
我只是想我的答案张贴到解决方案,因为我无法找到谷歌搜索通过和stackoverflowing :-)解决感叹......!这么多的分享...
hfossli

12
提出您认为有趣的问题并且已经有解决方案的问题就很好了。但是,请在措辞上加倍努力,以使听起来确实像是一个问题。
博佐

1
很棒的质量检查,谢谢hfossil!
Fattie

Answers:


71

通常,有些通知或其他可观察到的事件不支持KVO。即使文档说“ no”,从表面上看,观察支持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)。

注意:请注意,在其最终值稳定之前,您将收到多个回调。例如,更改视图或图层的框架将导致图层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");
}];

如果一个视图的框架国际志愿者组织兼容的,这是足够的观察只是框架。影响框架的其他属性也会在框架上触发更改通知(这将是一个从属键)。但是,正如我所说,所有这些都不是事实,可能只是偶然地起作用。
Nikolai Ruhe 2013年

4
CALayer.h说得好:“ CALayer为该类及其子类定义的所有Objective C属性实现标准的NSKeyValueCoding协议...”因此,您就可以了。:)它们是可观察的。
hfossli 2013年

2
正确的是,Core Animation对象已被证明符合KVC。但是,这与KVO合规性无关。KVC和KVO只是不同的东西(即使KVC合规性是KVO合规性的先决条件)。
Nikolai Ruhe 2013年

3
我要提请您注意使用KVO的方法附带的问题。我试图解释一个工作示例不支持有关如何在代码中正确执行操作的建议。如果您不相信这里还有另外一个参考,那就是不可能观察到任意UIKit属性
Nikolai Ruhe 2013年

5
请传递一个有效的上下文指针。这样做可以区分观察结果和其他对象的观察结果。不这样做可能导致不确定的行为,尤其是在删除观察者时。
2014年

62

编辑:我认为此解决方案不够彻底。保留此答案是出于历史原因。在这里查看我最新的答案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];
        }
    }
}

 

不起作用 您可以为UIView中的大多数属性添加观察者,但不能为框架添加观察者。我收到有关“可能未定义的关键路径'frame'”的编译器警告。忽略此警告并以任何方式执行此操作,永远不会调用observeValueForKeyPath方法。
n13 2012年

好吧,为我工作。我现在也在这里发布了更高版本和更强大的版本。
hfossli 2012年

3
确认,对我也适用。UIView.frame是正确可见的。有趣的是,UIView.bounds不是。
直到2012年

1
@hfossli没错,您不能盲目地调用[super]-这将引发类似“已收到...但未处理消息”的异常,这有点可耻-您必须实际上知道超类在调用它之前就实现了该方法。
理查德

3
-1:既不UIViewController声明view也不UIView声明frame是KVO兼容密钥。可可和可可触摸不允许任意观察按键。所有可观察到的键必须正确记录。看起来可行的事实并不能使这种有效(生产安全)的方式来观察视图中的帧变化。
Nikolai Ruhe

7

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

文档中

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

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

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


我同意UIView属性“框架”上的KVO。我提供的另一个答案似乎很完美。
hfossli 2013年

@hfossli ReactiveCocoa基于KVO构建。它具有相同的局限性和问题。这不是观察视图框架的正确方法。
Nikolai Ruhe 2013年

是的,我知道。这就是为什么我写了您可以做普通的KVO的原因。使用ReactiveCocoa只是为了保持简单。
hfossli 2013年

@NikolaiRuhe大家好-它只是发生在我身上。如果Apple无法KVO框架,他们将如何实现stackoverflow.com/a/25727788/294884现代视图约束?!
Fattie

@JoeBlow苹果不必使用KVO。他们控制所有方案的实施,UIView因此可以使用他们认为合适的任何机制。
Nikolai Ruhe 2014年

4

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

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

当我第一次将iOS 5指定为要编写代码的API时,这在iOS 4上对我来说是成功的,因此,需要临时仿真viewDidLayoutSubviews(尽管重写layoutSubviews更合适,但您明白了)。


您还需要对图层进行子类化才能获得transform等等
hfossli 2014年

这是一个很好的,可重复使用的(给您的UIView子类一个init方法,该方法接受视图以建立约束,视图控制器将更改报告回去,并且可以轻松地将其部署到所需的任何位置)解决方案仍然有效(发现覆盖setBounds :对我而言最有效)。当您由于需要重新布局元素而无法使用viewDidLayoutSubviews:方法时特别方便。
bcl

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

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

除了您不仅需要麻烦setFrame,而且还需要麻烦layer.bounds,layer.transform,layer.position,layer.zPosition,layer.anchorPoint,layer.anchorPointZ和layer.frame。KVO怎么了?:)
hfossli

0

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

如果您只想知道什么时候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可以更改

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)

-1

有一种方法可以完全不使用KVO,并且为了其他人找到这篇文章,我将在这里添加它。

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

尼克·洛克伍德(Nick Lockwood)撰写的这篇出色的教程介绍了如何使用核心动画定时功能来驱动任何东西。它比使用计时器或CADisplay层优越得多,因为您可以使用内置的计时功能,也可以轻松地创建自己的三次贝塞尔曲线功能(请参阅随附的文章(http://www.objc.io/issue-12/ animations-explained.html)。


“有一种无需使用KVO即可实现这一目标的方法”。在这种情况下,“这”是什么?您能具体一点吗?
hfossli 2014年

OP要求一种在动画时获取视图特定值的方法。他们还询问是否有可能对这些属性进行KVO设置,但确实没有技术支持。我建议您查看这篇文章,该文章为该问题提供了一个可靠的解决方案。
Sam Clewlow 2014年

你可以说得更详细点吗?您觉得文章的哪一部分相关?
hfossli

@hfossli看着它,我想我可能已经回答了一个错误的问题,因为我可以看到动画的任何内容!抱歉!
山姆·克洛洛

:-) 没问题。我只是渴望知识。
hfossli 2015年

-4

在某些UIKit属性中使用KVO是不安全的frame。或者至少这就是苹果所说的。

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

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

4
但是,@ NikolaiRuhe说:“ ReactiveCocoa基于KVO构建。它具有相同的局限性和问题。这不是观察视图框架的正确方法”
Fattie 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.