¿Cómo puedo realizar la observación de valores clave y obtener una devolución de llamada de KVO en el marco de una UIView?

79

Quiero ver los cambios en una UIView's frame, boundso de centerpropiedad. ¿Cómo puedo utilizar la observación de valores-clave para lograrlo?

hfossli
fuente
3
En realidad, esto no es una pregunta.
extremeboredom
7
solo quería publicar mi respuesta a la solución ya que no pude encontrar la solución buscando en Google y stackoverflowing :-) ... ¡suspiro! ... tanto para compartir ...
hfossli
12
Está perfectamente bien hacer preguntas que le parezcan interesantes y para las que ya tenga soluciones. Sin embargo, esfuércese más en formular la pregunta para que realmente suene como una pregunta que uno haría.
Bozho
1
Fantástico QA, gracias hfossil !!!
Fattie

Respuestas:

71

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 positiony 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 boundspuedes 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 framepuedes 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");
}];
hfossli
fuente
Si el marco de una vista fuera compatible con KVO, sería suficiente observar solo el marco. Otras propiedades que influyen en el marco también activarían una notificación de cambio en el marco (sería una clave dependiente). Pero, como dije, todo eso simplemente no es el caso y podría funcionar solo por accidente.
Nikolai Ruhe
4
Bueno, CALayer.h dice "CALayer implementa el protocolo estándar NSKeyValueCoding para todas las propiedades de Objective C definidas por la clase y sus subclases ..." Así que ahí lo tienes. :) Son observables.
hfossli
2
Tiene razón en que los objetos Core Animation están documentados para ser compatibles con KVC. Sin embargo, esto no dice nada sobre el cumplimiento de KVO. KVC y KVO son cosas diferentes (aunque el cumplimiento de KVC es un requisito previo para el cumplimiento de KVO).
Nikolai Ruhe
3
Estoy votando negativamente para llamar la atención sobre los problemas asociados con su enfoque de uso de KVO. Intenté explicar que un ejemplo funcional no admite una recomendación de cómo hacer algo correctamente en el código. En caso de que no esté convencido, aquí hay otra referencia al hecho de que no es posible observar propiedades arbitrarias de UIKit .
Nikolai Ruhe
5
Pase un puntero de contexto válido. Hacerlo le permite distinguir entre sus observaciones y las de algún otro objeto. No hacerlo puede resultar en un comportamiento indefinido, en particular en la eliminación de un observador.
sofocar el
62

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

 
hfossli
fuente
No funciona. Puede agregar observadores para la mayoría de las propiedades en UIView, pero no para el marco. Recibo una advertencia del compilador sobre una "ruta de acceso clave posiblemente indefinida 'marco'". Ignorando esta advertencia y haciéndolo de todos modos, nunca se llama al método observeValueForKeyPath.
n13
Bueno, funciona para mí. También he publicado una versión posterior y más robusta aquí.
hfossli
3
Confirmado, también funciona para mí. UIView.frame se puede observar correctamente. Curiosamente, UIView.bounds no lo es.
Hasta el
1
@hfossli Tienes razón en que no puedes llamar a ciegas a [super], eso generará una excepción como '... el mensaje fue recibido pero no manejado', lo cual es un poco vergonzoso, tienes que hacerlo saber realmente que la superclase implementa el método antes de llamarlo.
Richard
3
-1: Ni UIViewControllerdeclara viewni UIViewdeclara frameser 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.
Nikolai Ruhe
7

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 :

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

Hay algunas excepciones a esta regla, como la operationspropiedad 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.

Nikolai Ruhe
fuente
Estoy de acuerdo con usted sobre KVO en la propiedad "marco" en UIView. La otra respuesta que proporcioné parece funcionar perfectamente.
hfossli
@hfossli ReactiveCocoa se basa en KVO. Tiene las mismas limitaciones y problemas. No es una forma adecuada de observar el marco de una vista.
Nikolai Ruhe
Si lo se. Por eso escribí que podrías hacer KVO ordinario. Usar ReactiveCocoa fue solo para mantenerlo simple.
hfossli
Hola @NikolaiRuhe, se me acaba de ocurrir. Si Apple no puede KVO el marco, ¿cómo diablos implementan stackoverflow.com/a/25727788/294884 restricciones modernas para las vistas?
Fattie
@JoeBlow Apple no tiene que usar KVO. Controlan la implementación de todo UIViewpara que puedan usar cualquier mecanismo que consideren adecuado.
Nikolai Ruhe
4

Si pudiera contribuir a la conversación: como otros han señalado, frameno se garantiza que sea un valor clave observable en sí mismo y tampoco lo son las CALayerpropiedades, aunque parezcan serlo.

Lo que puede hacer en su lugar es crear una UIViewsubclase personalizada que anule setFrame:y anuncie ese recibo a un delegado. Configure el autoresizingMaskpara que la vista tenga todo flexible. Configúrelo para que sea completamente transparente y pequeño (para ahorrar costos en el CALayerrespaldo, 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ón layoutSubviewsera más apropiada, pero entiendes el punto).

Tommy
fuente
También necesitaría subclasificar la capa para obtener, transformetc.
hfossli
Esta es una solución agradable y reutilizable (proporcione a su subclase UIView un método de inicio que tome la vista para construir restricciones y el controlador de vista para informar los cambios y es fácil de implementar en cualquier lugar que lo necesite) solución que aún funciona (se encuentra anulando setBounds : más eficaz en mi caso). Especialmente útil cuando no puede usar el enfoque viewDidLayoutSubviews: debido a la necesidad de retransmitir elementos.
bcl
0

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

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));
}
Werner Altewischer
fuente
Excepto que necesitaría no solo swizzle setFrame, sino también layer.bounds, layer.transform, layer.position, layer.zPosition, layer.anchorPoint, layer.anchorPointZ y layer.frame. ¿Qué pasa con KVO? :)
hfossli
0

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 boundspuedes hacer cambios

Observable.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 framepuedes hacer cambios

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)
Perla Negra
fuente
-1

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

Sam Clewlow
fuente
"Hay una manera de lograr esto sin usar KVO". ¿Qué es "esto" en este contexto? Podrías ser un poco más específico.
hfossli
El OP solicitó una forma de obtener valores específicos para una vista mientras se está animando. También preguntaron si es posible KVO estas propiedades, que es, pero no es técnicamente compatible. He sugerido consultar el artículo, que proporciona una solución sólida al problema.
Sam Clewlow
¿Puedes ser mas específico? ¿Qué parte del artículo te pareció relevante?
hfossli
@hfossli Mirándolo, creo que podría haber planteado esta respuesta a la pregunta incorrecta, ¡ya que puedo ver cualquier mención de animaciones! ¡Lo siento!
Sam Clewlow
:-) No hay problema. Solo tenía hambre de conocimiento.
hfossli
-4

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
}];
saky
fuente
4
sin embargo, @NikolaiRuhe dice "ReactiveCocoa se basa en KVO. Tiene las mismas limitaciones y problemas. No es una forma adecuada de observar el marco de una vista"
Fattie