Quiero ver los cambios en una UIView
's frame
, bounds
o de center
propiedad. ¿Cómo puedo utilizar la observación de valores-clave para lograrlo?
iphone
ios
objective-c
uiview
key-value-observing
hfossli
fuente
fuente
Respuestas:
Por lo general, hay notificaciones u otros eventos observables en los que no se admite KVO. Aunque los documentos dicen 'no' , es aparentemente seguro observar el CALayer respaldando la UIView. La observación de CALayer funciona en la práctica debido a su uso extensivo de KVO y los accesos adecuados (en lugar de la manipulación de ivar). No está garantizado que funcione en el futuro.
De todos modos, el marco de la vista es solo el producto de otras propiedades. Por lo tanto, debemos observar aquellos:
[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];
Vea el ejemplo completo aquí https://gist.github.com/hfossli/7234623
NOTA: No se dice que esto sea compatible con los documentos, pero funciona a partir de hoy con todas las versiones de iOS hasta ahora (actualmente iOS 2 -> iOS 11)
NOTA: Tenga en cuenta que recibirá varias devoluciones de llamada antes de que se establezca en su valor final. Por ejemplo, cambiar el marco de una vista o capa hará que la capa cambie
position
ybounds
(en ese orden).Con ReactiveCocoa puedes hacer
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"); }];
Y si solo quieres saber cuándo
bounds
puedes hacer cambios@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"); }];
Y si solo quieres saber cuándo
frame
puedes hacer cambios@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"); }];
fuente
EDITAR : No creo que esta solución sea lo suficientemente completa. Esta respuesta se mantiene por razones históricas. Vea mi respuesta más reciente aquí: https://stackoverflow.com/a/19687115/202451
Tienes que hacer KVO en la propiedad del marco. "self" es en este caso un UIViewController.
agregando el observador (generalmente hecho en viewDidLoad):
[self addObserver:self forKeyPath:@"view.frame" options:NSKeyValueObservingOptionOld context:NULL];
eliminar el observador (normalmente se hace en dealloc o viewDidDisappear :):
[self removeObserver:self forKeyPath:@"view.frame"];
Obtener información sobre el cambio
- (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]; } } }
fuente
UIViewController
declaraview
niUIView
declaraframe
ser claves compatibles con KVO. Cocoa y Cocoa-touch no permiten la observación arbitraria de las teclas. Todas las claves observables deben estar debidamente documentadas. El hecho de que parezca funcionar no significa que sea una forma válida (segura para la producción) de observar cambios de marco en una vista.Actualmente no es posible usar KVO para observar el marco de una vista. Las propiedades deben ser compatibles con KVO para ser observables. Lamentablemente, las propiedades del marco UIKit generalmente no son observables, como ocurre con cualquier otro marco del sistema.
De la documentación :
Hay algunas excepciones a esta regla, como la
operations
propiedad de NSOperationQueue, pero deben documentarse explícitamente.Incluso si el uso de KVO en las propiedades de una vista podría funcionar actualmente, no recomendaría usarlo en el código de envío. Es un enfoque frágil y se basa en un comportamiento indocumentado.
fuente
UIView
para que puedan usar cualquier mecanismo que consideren adecuado.Si pudiera contribuir a la conversación: como otros han señalado,
frame
no se garantiza que sea un valor clave observable en sí mismo y tampoco lo son lasCALayer
propiedades, aunque parezcan serlo.Lo que puede hacer en su lugar es crear una
UIView
subclase personalizada que anulesetFrame:
y anuncie ese recibo a un delegado. Configure elautoresizingMask
para que la vista tenga todo flexible. Configúrelo para que sea completamente transparente y pequeño (para ahorrar costos en elCALayer
respaldo, no es que importe mucho) y agréguelo como una subvista de la vista en la que desea ver los cambios de tamaño.Esto funcionó con éxito para mí en iOS 4 cuando por primera vez especificamos iOS 5 como la API para codificar y, como resultado, necesitábamos una emulación temporal de
viewDidLayoutSubviews
(aunque la anulaciónlayoutSubviews
era más apropiada, pero entiendes el punto).fuente
transform
etc.Como se mencionó, si KVO no funciona y solo desea observar sus propias vistas sobre las que tiene control, puede crear una vista personalizada que anule setFrame o setBounds. Una advertencia es que el valor de marco final deseado puede no estar disponible en el momento de la invocación. Por lo tanto, agregué una llamada GCD al siguiente ciclo del hilo principal para verificar el valor nuevamente.
-(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... } }); }
fuente
Para no confiar en la observación de KVO, puede realizar el método swizzling de la siguiente manera:
@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
Ahora para observar el cambio de marco para una UIView en el código de su aplicación:
- (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)); }
fuente
Respuesta de @hfossli actualizada para RxSwift y Swift 5 .
Con RxSwift puedes hacer
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)
Y si solo quieres saber cuándo
bounds
puedes hacer cambiosObservable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.bounds))).subscribe(onNext: { _ in print("View bounds changed its geometry") }).disposed(by: rx.disposeBag)
Y si solo quieres saber cuándo
frame
puedes hacer cambiosObservable.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)
fuente
Hay una manera de lograr esto sin usar KVO en absoluto, y para que otros encuentren esta publicación, la agregaré aquí.
http://www.objc.io/issue-12/animating-custom-layer-properties.html
Este excelente tutorial de Nick Lockwood describe cómo usar las funciones de sincronización de las animaciones centrales para manejar cualquier cosa. Es muy superior a usar un temporizador o una capa CADisplay, porque puede usar las funciones de temporización integradas o crear con bastante facilidad su propia función bezier cúbica (consulte el artículo adjunto ( http://www.objc.io/issue-12/ animaciones-explicadas.html ).
fuente
No es seguro usar KVO en algunas propiedades de UIKit como
frame
. O al menos eso es lo que dice Apple.Recomendaría usar ReactiveCocoa , esto lo ayudará a escuchar los cambios en cualquier propiedad sin usar KVO, es muy fácil comenzar a observar algo usando Signals:
[RACObserve(self, frame) subscribeNext:^(CGRect frame) { //do whatever you want with the new frame }];
fuente