iphone ios objective-c uiview key-value-observing

iphone - ¿Cómo puedo hacer Key Value Observing y obtener una devolución de llamada de KVO en el marco de UIView?



ios objective-c (8)

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 con cualquier otro marco de sistema.

De la documentation :

Nota: Aunque las clases del marco UIKit generalmente no son compatibles con KVO, puede implementarlo en los objetos personalizados de su aplicación, incluidas las vistas personalizadas.

Existen algunas excepciones a esta regla, como la propiedad de operations de NSOperationQueue, pero deben documentarse explícitamente.

Incluso si el uso de KVO en las propiedades de una vista actualmente funcionara, no recomendaría su uso en el código de envío. Es un enfoque frágil y se basa en el comportamiento indocumentado.

Quiero ver cambios en el frame , los bounds o la propiedad del center UIView . ¿Cómo puedo usar Key-Value Observing para lograr esto?


Como se mencionó, si KVO no funciona y usted 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 último valor de fotograma deseado puede no estar disponible en el punto de invocación. Por lo tanto, agregué una llamada GCD al siguiente ciclo principal del hilo 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... } }); }


Generalmente hay notificaciones u otros eventos observables en los que KVO no es compatible. Aunque los documentos dicen "no" , es aparentemente seguro observar al CALayer que respalda la UIView. Observando los trabajos de CALayer en la práctica debido a su uso extensivo de KVO y accedores adecuados (en lugar de manipulación de ivar). No se garantiza que funcione en el futuro.

De todos modos, el marco de la vista es solo el producto de otras propiedades. Por lo tanto, debemos observarlos:

[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: Esto no se dice que es compatible con los documentos, pero funciona a partir de hoy con todas las versiones de iOS hasta el momento (actualmente iOS 2 -> iOS 11)

NOTA: tenga en cuenta que recibirá múltiples devoluciones de llamada antes de que se estabilice en su valor final. Por ejemplo, al cambiar el marco de una vista o capa, la capa cambiará de position y bounds (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 cambian los bounds puedes hacerlo

@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 cambia el frame puedes hacerlo

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


Hay una manera de lograr esto sin usar KVO en absoluto, y por el bien de otros que 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 animaciones centrales para conducir cualquier cosa. Es muy superior al uso de un temporizador o una capa CADisplay, ya que puede utilizar 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/animations-explained.html ).


No es seguro usar KVO en algunas propiedades de UIKit como frame . O al menos eso es lo que dice Apple.

Recomiendo usar ReactiveCocoa , esto te 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 }];


Para no confiar en la observación de KVO, podría 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 su código de 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)); }


Si pudiera contribuir a la conversación: como han señalado otros, frame no garantiza que sea clave-valor observable en sí y tampoco lo son las propiedades CALayer aunque parezcan serlo.

Lo que puede hacer en su lugar es crear una subclase UIView personalizada que anule a setFrame: y anuncie ese recibo a un delegado. Configure el autoresizingMask para que la vista tenga todo flexible. Configúrelo para que sea completamente transparente y pequeño (para ahorrar costos en el respaldo de CALayer , 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 en iOS 4 cuando estábamos especificando iOS 5 como la API para codificar y, como resultado, necesitábamos una emulación temporal de viewDidLayoutSubviews (aunque la anulación de layoutSubviews era más apropiada, pero entiendes el punto) .


EDITAR : No creo que esta solución sea lo suficientemente profunda. Esta respuesta se guarda por razones históricas. Vea mi respuesta más reciente aquí: https://.com/a/19687115/202451

Tienes que hacer KVO en la propiedad frame. "self" es en este caso un UIViewController.

agregando el observador (típicamente hecho en viewDidLoad):

[self addObserver:self forKeyPath:@"view.frame" options:NSKeyValueObservingOptionOld context:NULL];

eliminando el observador (típicamente hecho 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]; } } }