Mettre en œuvre un motif Debumed / Coalesced dans Cocoa Touch comme `layoutSubviews`

Un certain nombre de classs Cocoa Touch s'appuient sur un model de design d'events coalescents. UIViews , par exemple, ont une méthode setNeedsLayout qui provoque l' layoutSubviews de layoutSubviews dans un très proche avenir. Ceci est particulièrement utile dans les situations où un certain nombre de propriétés influencent la layout. Dans le setter pour chaque propriété, vous pouvez appeler [self setNeedsLayout] qui assurera que la layout sera mise à jour, mais empêchera de nombreuses mises à jour (potentiellement coûteuses) si plusieurs propriétés sont changées à la fois ou même si une seule propriété a été modifiée fois dans une itération de la boucle d'exécution. D'autres opérations coûteuses comme setNeedsDisplay et drawRect: paire de methods suivent le même model.

Quelle est la meilleure façon de mettre en œuvre un model comme celui-ci? Plus précisément, je voudrais lier un certain nombre de propriétés dépendantes à une méthode coûteuse qui doit être appelée une fois par itération de la boucle d'exécution si une propriété a changé.


Solutions possibles

En utilisant un CADisplayLink ou un NSTimer vous pouvez get quelque chose qui fonctionne comme cela, mais les deux semblent plus impliqués que nécessaire et je ne suis pas sûr de ce que les implications de performance de l'ajout de beaucoup d'objects (en particulier les minuteurs). Après tout, la performance est la seule raison de faire quelque chose comme ça.

J'ai utilisé quelque chose comme ça dans certains cas:

 - (void)debounceSelector:(SEL)sel withDelay:(CGFloat)delay { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:sel object:nil]; [self performSelector:sel withObject:nil afterDelay:delay]; } 

Cela fonctionne très bien dans les situations où une input de l'user ne devrait triggersr un événement que lorsqu'une action continue, ou des choses comme ça. Il semble maladroit quand nous voulons nous assurer qu'il n'y a pas de retard dans le triggersment de l'événement, mais que nous voulons simplement merge les appels dans la même boucle d'exécution.

NSNotificationQueue a exactement ce que vous cherchez. Voir la documentation sur les notifications Coalescing

Voici un exemple simple dans un UIViewController:

 - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(configureView:) name:@"CoalescingNotificationName" object:self]; [self setNeedsReload:@"viewDidLoad1"]; [self setNeedsReload:@"viewDidLoad2"]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self setNeedsReload:@"viewWillAppear1"]; [self setNeedsReload:@"viewWillAppear2"]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self setNeedsReload:@"viewDidAppear1"]; [self setNeedsReload:@"viewDidAppear2"]; } - (void)setNeedsReload:(NSSsortingng *)context { NSNotification *notification = [NSNotification notificationWithName:@"CoalescingNotificationName" object:self userInfo:@{@"context":context}]; [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName|NSNotificationCoalescingOnSender forModes:nil]; } - (void)configureView:(NSNotification *)notification { NSSsortingng *text = [NSSsortingng ssortingngWithFormat:@"configureView called: %@", notification.userInfo]; NSLog(@"%@", text); self.detailDescriptionLabel.text = text; } 

Vous pouvez consulter les documents et jouer avec le style de publication pour get le comportement souhaité. L'utilisation de NSPostASAP , dans cet exemple, nous donnera la sortie:

 configureView called: { context = viewDidLoad1; } configureView called: { context = viewDidAppear1; } 

Cela signifie que les appels setNeedsReload à setNeedsReload ont été fusionnés.

J'ai implémenté quelque chose comme ça en utilisant des sources de dispatch personnalisées. Fondamentalement, vous configurez une source de répartition en utilisant DISPATCH_SOURCE_TYPE_DATA_OR comme tel:

 dispatch_source_t source = dispatch_source_create( DISPATCH_SOURCE_TYPE_DATA_OR, 0, 0, dispatch_get_main_queue() ); dispatch_source_set_event_handler( source, ^{ // UI update logic goes here }); dispatch_resume( source ); 

Après cela, chaque fois que vous voulez signaler qu'il est time de mettre à jour, vous appelez:

 dispatch_source_merge_data( __source, 1 ); 

Le bloc du gestionnaire d'events est non réentrant, de sorte que les mises à jour qui se produisent pendant l'exécution du gestionnaire d'events coalescent.

Ceci est un model que j'utilise un peu juste dans mon cadre, Conche ( https://github.com/djs-code/Conche ). Si vous cherchez d'autres exemples, piquez autour de CNCHStateMachine.m et CNCHObjectFeed.m.

Cela frise "principalement l'opinion basée", mais je vais jeter ma méthode habituelle de manipulation de ceci:

Définissez un indicateur, puis mettez en queue le traitement avec performSelector.

Dans votre @interface mis:

 @property (nonatomic, readonly) BOOL needsUpdate; 

Et puis dans votre @implementation mettez:

 -(void)setNeedsUpdate { if(!_needsUpdate) { _needsUpdate = true; [self performSelector:@selector(_performUpdate) withObject:nil afterDelay:0.0]; } } -(void)_performUpdate { if(_needsUpdate) { _needsUpdate = false; [self performUpdate]; } } -(void)performUpdate { } 

La double vérification de _needsUpdate est un peu redondante, mais bon marché. Le véritable paranoïaque engloberait toutes les parties pertinentes dans @synchronized, mais ce n'est vraiment nécessaire que si setNeedsUpdate peut être invoqué à partir de threads autres que le thread principal. Si vous faites cela, vous devez également modifier setNeedsUpdate pour accéder au thread principal avant d'appeler performSelector.

Je crois comprendre qu'appeler performSelector:withObject:afterDelay: utilisant une valeur de retard de 0, la méthode est appelée lors de la prochaine passe dans la boucle d'events.

Si vous voulez que vos actions soient mises en queue jusqu'au prochain passage dans la boucle d'events, cela devrait fonctionner correctement.

Si vous voulez merge plusieurs actions différentes et que vous voulez seulement un appel "faire tout ce qui s'est accumulé depuis la dernière passe via la boucle d'événement", vous pouvez append un appel unique à performSelector:withObject:afterDelay: dans votre délégué d'application (ou autre instance unique object) au lancement, et invoquez votre méthode à la fin de chaque appel. Vous pouvez ensuite append un NSMutableSet de choses à faire et append une input à l'set chaque fois que vous triggersz une action que vous voulez merge. Si vous avez créé un object d'action personnalisé et remplacez les methods isEqual (et hash) sur votre object d'action, vous pouvez le configurer pour qu'il n'y ait plus qu'un seul object d'action de chaque type dans votre set d'actions. Ajouter le même type d'action plusieurs fois dans un passage à travers la boucle d'events appendait une et une seule action de ce type).

Votre méthode pourrait ressembler à ceci:

 - (void) doCoalescedActions; { for (CustomActionObject *aCustomAction in setOfActions) { //Do whatever it takes to handle coalesced actions } [setOfActions removeAllObjects]; [self performSelector: @selector(doCoalescedActions) withObject: nil afterDelay: 0]; } 

Il est difficile d'entrer dans les détails sur la façon de le faire sans les détails spécifiques de ce que vous voulez faire.