web-dev-qa-db-fra.com

Comment puis-je faire l'observation des valeurs clés et obtenir un rappel KVO sur la trame UIView?

Je souhaite surveiller les modifications dans une propriété UIViewframe, bounds ou center. Comment puis-je utiliser l'observation des valeurs-clés pour y parvenir?

75
hfossli

Il existe généralement des notifications ou d'autres événements observables où KVO n'est pas pris en charge. Même si les documents disent 'non' , il est apparemment sûr d'observer le CALayer soutenant l'UIView. L'observation du CALayer fonctionne dans la pratique en raison de son utilisation intensive du KVO et des accessoires appropriés (au lieu de la manipulation d'ivar). Il n'est pas garanti de fonctionner à l'avenir.

Quoi qu'il en soit, le cadre de la vue n'est que le produit d'autres propriétés. Par conséquent, nous devons observer ceux-ci:

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

Voir l'exemple complet ici https://Gist.github.com/hfossli/723462

REMARQUE: Il n'est pas dit que cela soit pris en charge dans les documents, mais cela fonctionne à ce jour avec toutes les versions iOS jusqu'à présent (actuellement iOS 2 -> iOS 11)

REMARQUE: sachez que vous recevrez plusieurs rappels avant qu'il ne se règle à sa valeur finale. Par exemple, la modification du cadre d'une vue ou d'un calque entraîne la modification du calque position et bounds (dans cet ordre).


Avec ReactiveCocoa vous pouvez faire

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

Et si vous voulez seulement savoir quand bounds change vous pouvez faire

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

Et si vous voulez seulement savoir quand frame change vous pouvez faire

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

[~ # ~] modifier [~ # ~] : Je ne pense pas que cette solution soit suffisamment approfondie. Cette réponse est conservée pour des raisons historiques. Voir ma dernière réponse ici: https://stackoverflow.com/a/19687115/202451


Vous devez faire KVO sur la propriété frame. "self" est dans ce cas un UIViewController.

ajout de l'observateur (généralement effectué dans viewDidLoad):

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

supprimer l'observateur (généralement effectué dans dealloc ou viewDidDisappear :):

[self removeObserver:self forKeyPath:@"view.frame"];

Obtenir des informations sur le changement

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

 
62
hfossli

Actuellement, il n'est pas possible d'utiliser KVO pour observer le cadre d'une vue. Les propriétés doivent être conformes à KVO pour être observables. Malheureusement, les propriétés du cadre UIKit ne sont généralement pas observables, comme avec tout autre cadre système.

De la documentation :

Remarque: Bien que les classes du framework UIKit ne prennent généralement pas en charge KVO, vous pouvez toujours l'implémenter dans les objets personnalisés de votre application, y compris les vues personnalisées.

Il existe quelques exceptions à cette règle, comme la propriété operations de NSOperationQueue, mais elles doivent être explicitement documentées.

Même si l'utilisation de KVO sur les propriétés d'une vue peut actuellement fonctionner, je ne recommanderais pas de l'utiliser dans le code d'expédition. C'est une approche fragile et repose sur un comportement sans papiers.

7
Nikolai Ruhe

Si je peux contribuer à la conversation: comme d'autres l'ont souligné, frame n'est pas garanti d'être une valeur-clé elle-même observable et les propriétés CALayer ne le sont pas non plus même si elles semblent l'être.

Ce que vous pouvez faire à la place est de créer une sous-classe UIView personnalisée qui remplace setFrame: et annonce cette réception à un délégué. Définissez autoresizingMask pour que la vue ait tout flexible. Configurez-le pour qu'il soit entièrement transparent et petit (pour économiser des coûts sur le support CALayer, pas que cela compte beaucoup) et ajoutez-le comme sous-vue de la vue sur laquelle vous souhaitez regarder les changements de taille.

Cela a fonctionné avec succès pour moi sous iOS 4 lorsque nous avons d'abord spécifié iOS 5 comme API pour coder et, par conséquent, nous avions besoin d'une émulation temporaire de viewDidLayoutSubviews (bien que cette surcharge layoutSubviews était plus approprié, mais vous obtenez le point).

4
Tommy

Il existe un moyen d'y parvenir sans utiliser KVO du tout, et pour le bien des autres qui trouveront ce message, je vais l'ajouter ici.

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

Cet excellent tutoriel de Nick Lockwood décrit comment utiliser les fonctions de synchronisation des animations de base pour piloter quoi que ce soit. C'est de loin supérieur à l'utilisation d'une minuterie ou d'une couche CADisplay, car vous pouvez utiliser les fonctions de synchronisation intégrées ou créer assez facilement votre propre fonction de Bézier cubique (voir l'article qui l'accompagne ( http://www.objc.io/ issue-12/animations-expliqué.html ).

0
Sam Clewlow

Pour ne pas vous fier à l'observation du KVO, vous pouvez effectuer le swizzling de la méthode comme suit:

@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

Maintenant, pour observer le changement de trame pour une UIView dans votre code d'application:

- (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));
}
0
Werner Altewischer

Comme mentionné, si KVO ne fonctionne pas et que vous souhaitez simplement observer vos propres vues sur lesquelles vous avez le contrôle, vous pouvez créer une vue personnalisée qui remplace setFrame ou setBounds. Une mise en garde est que la valeur de trame finale souhaitée peut ne pas être disponible au point d'invocation. J'ai donc ajouté un appel GCD à la prochaine boucle de thread principal pour vérifier à nouveau la valeur.

-(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
loungerdork