我想看了在变化UIView
的frame
,bounds
或center
财产。如何使用键值观察来实现这一目标?
Answers:
通常,有些通知或其他可观察到的事件不支持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)。
注意:请注意,在其最终值稳定之前,您将收到多个回调。例如,更改视图或图层的框架将导致图层position
和bounds
(按此顺序)更改。
使用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");
}];
编辑:我认为此解决方案不够彻底。保留此答案是出于历史原因。在这里查看我最新的答案: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];
}
}
}
UIViewController
声明view
也不UIView
声明frame
是KVO兼容密钥。可可和可可触摸不允许任意观察按键。所有可观察到的键必须正确记录。看起来可行的事实并不能使这种有效(生产安全)的方式来观察视图中的帧变化。
当前,无法使用KVO观察视图的框架。属性必须符合KVO才能观察。可悲的是,与其他系统框架一样,UIKit框架的属性通常不可观察。
从文档中:
注意:尽管UIKit框架的类通常不支持KVO,但是您仍然可以在应用程序的自定义对象中实现它,包括自定义视图。
此规则有一些例外,例如NSOperationQueue的operations
属性,但是必须明确记录它们。
即使在视图属性上使用KVO当前可能有效,我也不建议在运输代码中使用它。这是一种脆弱的方法,并且依赖于未记录的行为。
UIView
因此可以使用他们认为合适的任何机制。
如果我可以为对话做出贡献:正如其他人所指出的那样,frame
就不能保证键值本身是可观察的,CALayer
即使看起来像属性,也不能保证它们是可观察的。
相反,您可以做的是创建一个自定义UIView
子类,该子类将覆盖setFrame:
该收据并将其声明给委托人。设置,autoresizingMask
以便视图具有灵活的所有内容。将其配置为完全透明且较小(以节省CALayer
支持成本,但并不重要),并将其添加为您要观看其尺寸变化的视图的子视图。
当我第一次将iOS 5指定为要编写代码的API时,这在iOS 4上对我来说是成功的,因此,需要临时仿真viewDidLayoutSubviews
(尽管重写layoutSubviews
更合适,但您明白了)。
transform
等等
如前所述,如果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观察,您可以执行如下方法刷新:
@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));
}
更新了RxSwift和Swift 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)
有一种方法可以完全不使用KVO,并且为了其他人找到这篇文章,我将在这里添加它。
http://www.objc.io/issue-12/animating-custom-layer-properties.html
尼克·洛克伍德(Nick Lockwood)撰写的这篇出色的教程介绍了如何使用核心动画定时功能来驱动任何东西。它比使用计时器或CADisplay层优越得多,因为您可以使用内置的计时功能,也可以轻松地创建自己的三次贝塞尔曲线功能(请参阅随附的文章(http://www.objc.io/issue-12/ animations-explained.html)。
在某些UIKit属性中使用KVO是不安全的frame
。或者至少这就是苹果所说的。
我建议使用ReactiveCocoa,这将帮助您在不使用KVO的情况下监听任何属性的变化,使用Signals开始观察是非常容易的:
[RACObserve(self, frame) subscribeNext:^(CGRect frame) {
//do whatever you want with the new frame
}];