A number of Cocoa Touch classes leverage a design pattern of coalescing events. UIViews
, for example, have a method setNeedsLayout
which causes layoutSubviews
to be called in the very near future. This is especially useful in situations where a number of properties influence the layout. In the setter for each property you can call [self setNeedsLayout]
which will ensure the layout will be updated, but will prevent many (potentially expensive) updates to the layout if multiple properties are changed at once or even if a single property were modified multiple times within one iteration of the run loop. Other expensive operations like the setNeedsDisplay
and drawRect:
pair of methods follow the same pattern.
What's the best way to implement pattern like this? Specifically I'd like to tie a number of dependent properties to an expensive method that needs to be called once per iteration of the run loop if a property has changed.
Possible Solutions:
Using a CADisplayLink
or NSTimer
you could get something working like this, but both seem more involved than necessary and I'm not sure what the performance implications of adding this to lots of objects (especially timers) would be. After all, performance is the only reason to do something like this.
I've used something like this in some cases:
- (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 a user input should only trigger some event when a continuous action, or things like that. It seems clunky when we want to ensure there is no delay in triggering the event, instead we just want to coalesce calls within the same run loop.
NSNotificationQueue
has just the thing you're looking for. See the documentation on Coalescing Notifications
Here 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 checkout the docs and play with the postingStyle to get the behavior you desired. Using NSPostASAP
, in this example, will give us output:
configureView called: {
context = viewDidLoad1;
}
configureView called: {
context = viewDidAppear1;
}
meaning that back-to-back calls to setNeedsReload
have been coalesced.
I've implemented something like this using custom dispatch sources. Basically, you setup a 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 notify that it's time to update, you call:
dispatch_source_merge_data( __source, 1 );
The event handler block is non-reentrant, so updates that occur while the event handler is running will coalesce.
This is a pattern I use a fair bit in my framework, Conche (https://github.com/djs-code/Conche). If you're looking for other examples, poke around CNCHStateMachine.m and CNCHObjectFeed.m.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With