Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement a Debounced/Coalesced Pattern in Cocoa Touch like `layoutSubviews`

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.

like image 362
Anthony Mattox Avatar asked Jan 26 '15 15:01

Anthony Mattox


2 Answers

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.

like image 153
Joseph Lin Avatar answered Sep 28 '22 12:09

Joseph Lin


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.

like image 30
Dan Stenmark Avatar answered Sep 28 '22 12:09

Dan Stenmark