Implementing Debugg / Coalesced Pattern in Cocoa Touch like `layoutSubviews`

A number of Cocoa Touch classes use a coalescing event design pattern. UIViews , for example, has a setNeedsLayout method that calls layoutSubviews in the very near future. This is especially useful in situations where several properties affect the layout. In the customizer for each property, you can call [self setNeedsLayout] , which will provide a layout update, but will prevent many (potentially costly) layout updates if several properties change at once or even if one property has been changed several times within the same iteration of the launch cycle. Other expensive operations, such as a pair of setNeedsDisplay and drawRect: methods, follow the same pattern.

What is the best way to implement such a template? In particular, I would like to bind a number of dependent properties to an expensive method that needs to be called once to iterate through the execution cycle if the property has changed.


Possible solutions :

Using CADisplayLink or NSTimer , you can get something similar, but both of them seem more involved than necessary, and I'm not sure that the consequences for adding this large number of objects (especially timers) will be. In the end, performance is the only reason do something like this.

In some cases, I used something like this:

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

This works great in situations where user input should trigger an event only with continuous action or in such things. This seems awkward when we want to ensure that there is no delay in triggering the event, instead we just want to combine calls within a single execution loop.

+6
source share
4 answers

NSNotificationQueue has just what you are looking for. See Coovalcing Notifications Documentation

Here is a simple example in a 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:(NSString *)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 { NSString *text = [NSString stringWithFormat:@"configureView called: %@", notification.userInfo]; NSLog(@"%@", text); self.detailDescriptionLabel.text = text; } 

You can check the docs and play with postingStyle to get the desired behavior. Using NSPostASAP , in this example we get the output:

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

means setNeedsReload callbacks setNeedsReload been merged.

+3
source

I implemented something like this using custom send sources. Basically, you configure the dispatch source using DISPATCH_SOURCE_TYPE_DATA_OR as such:

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

After that, every time you want to report that it is time for an update, you call:

 dispatch_source_merge_data( __source, 1 ); 

The event handler block is not reentrant, so updates that occur during the execution of the event handler will be merged.

This is a template in which I use a fair bit in my structure, Conche ( https://github.com/djs-code/Conche ). If you're looking for other examples, go around CNCHStateMachine.m and CNCHObjectFeed.m.

+3
source

This borders on "mostly opinion-based," but I'll throw out my usual way of dealing with this:

Set the flag, and then process the queue using the performSelector function.

In the @interface put field:

 @property (nonatomic, readonly) BOOL needsUpdate; 

And then in your @implementation put:

 -(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 { } 

Double checking _needsUpdate little redundant, but cheap. Indeed, the paranoid will wrap all relevant parts in @ synchronized, but this is really only necessary if setNeedsUpdate can be called from threads other than the main thread. If you intend to do this, you also need to make changes to setNeedsUpdate to go to the main thread before calling the performSelector function.

0
source

I understand that calling performSelector:withObject:afterDelay: using a delay value of 0 results in a method call the next time the event loop passes.

If you want your actions to be queued before the next event loop, this should work fine.

If you want to combine several different actions and only want one โ€œto do everything that has been accumulated since the last pass through the loop callโ€, you could add one call to performSelector:withObject:afterDelay: in your applicationโ€™s delta (or some another separate instance of the object) when you start and call your method again at the end of each call. You can then add the NSMutableSet what you need to do and add an entry to the set each time you run the action you want to combine. If you created a custom action object and redefined the isEqual (and hash) methods in your action object, you can configure it to always have only one action object of each type in your action set. Adding the same type of action several times into the passage through the event loop will add one and only one action of this type).

Your method might look something like this:

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

Itโ€™s hard to talk in detail about how to do this without the specific details of what you want to do.

0
source

Source: https://habr.com/ru/post/981554/


All Articles