1

What is the best way to buffer incoming events/notifications your iPhone app is observing so as not to trigger event code for each event? A code sample would be great...

e.g. would it be to create an NSMutableArray in the controller to add each event to, and for each event that comes in it triggers a count down time for 2 seconds, so the timer countdown would trigger the events handling code...in which case what would be the best Objective-C classes to use for such a buffer/timer/countdown...

background - in my case it the events from "EKEventStoreChangedNotification" that I handle, but noting there are multiple that can come through for the same single calendar item change (well as far as i can tell)

Greg
  • 34,042
  • 79
  • 253
  • 454
  • Hi Greg, A clarification question - do you want to just serially delay the execution of handler, or actually queue the events for some time to filter them out before triggering handling code? – Sean S Lee Oct 19 '11 at 18:21
  • @Landasia - for my immediate project just the former, but I guess for a more generic answer the later would be nice (a value add) – Greg Oct 19 '11 at 23:06

5 Answers5

2

attach an object to your main run loop (typically via a timer). the object receives, collects, filters, and coalesces the events as needed.

something like this would be a starting point (not compiled):

@interface MONEventHandler : NSObject
{
    NSMutableArray * events;
    NSRecursiveLock * lock;
}

@end

@implementation MONEventHandler

- (id)init
{
    self = [super init];
    if (nil != self) {
        events = [NSMutableArray new];
        lock = [NSRecursiveLock new];
        /* begin listening for incoming events */
    }
    return self;
}

- (void)dealloc
{
/* stop listening for incoming events */
    [lock release], lock = nil;
    [events release], events = nil;
    [super dealloc];
}

- (void)postEvents:(NSTimer *)timer
{
    [lock lock];
    /* ... clear out self.events here ... */
    [lock unlock];
}

/* a few methods for your event entries, or callbacks */

@end

Now create the timer and add it to the main run loop:

/* call me on main thread ONLY */
void SetupEventHandler() {
    MONEventHandler * eventHandler = [MONEventHandler new];
    NSTimeInterval seconds = 0.100; /* 10Hz */
    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:seconds target:eventHandler selector:@selector(postEvents:) userInfo:nil repeats:YES];
    /* you will need timer if you want to explicitly stop updates or destroy eventHandler. you do this by invalidating the timer. you can also access it from postEvents: */
   [eventHandler release];
}
justin
  • 104,054
  • 14
  • 179
  • 226
  • Justin - just trying to work out which answer to TICK - is it really necessary to have the lock concept? @rpetrich 's answer does seem simpler and achieve the same thing no? – Greg Oct 20 '11 at 22:17
  • @Greg The lock may or may not be necessary: Do the APIs you use which pass events/notifications *guarantee* that these messages will all be from the main thread? Since you need the main tread to dispatch the results, any message from a secondary thread could ruin your data and put your program in an invalid state (depending on when it happened). Example: NSNotifications are posted from the client's thread; observers are messaged synchronously, in an undefined order, from the thread that posted the notification. As well, `NSMutableArray` is not thread safe (until you protect it with locks). – justin Oct 20 '11 at 22:29
  • If the APIs do not make that guarantee, then rpetrich's solution could fail on any line that `queuedEvents` is used, including indirectly (`for (id event in events)`). – justin Oct 20 '11 at 22:33
  • Justin - any comments on @Landasia 's recent answer using GCD? (which is new to me) How would you compare this as a answer to yours? Is there anything "better" with this approach over your use of "NSTimer" and "lock"? Your answer reads more easily to me, however probably because I haven't used GCD before perhaps... – Greg Oct 23 '11 at 22:41
  • @Greg I wrote: *Since you need the main thread to dispatch the results*... In retrospect, I don't see where you stated that requirement - I was (mistakenly) under the impression that it was a requirement. Moving along... Landasia's approach is also useful, but there are functionally quite different. `MONEventHandler.events` and Landasia's `NSMutableArray` serve similar purposes (you may or may not need explicit thread safety in this case). (cont) – justin Oct 24 '11 at 09:08
  • (cont) The *big* difference here is 1) how you want/need your events dispatched, and 2) statistically how you receive them. 1) my version messages via funneling through the main run loop and everything is coalesced and vended in batches on the main thread with distribution synchronized with the run loop's cycle, while Landasia's messages are made *asynchronously* after a period of time (50μs in the example, which is very fast). (cont) – justin Oct 24 '11 at 09:08
  • (cont) if your events need to update the UI, then you will have to go through the main run loop one way or another, and it's actually faster, more predictable, and offers a more appropriate execution sequence to use my approach in that case. Landasia's version is good for asynchronous updates which update frequently and independent of the main run loop. If async is what you want, you could easily adapt `MONEventHandler.events` to be emptied via an async `NSOperation` - when the timer ticks and there are events to post, then create an operation (). – justin Oct 24 '11 at 09:08
1

In your ivar declaration:

NSMutableArray *queuedEvents;

In your class implementation:

- (void)queueEvent:(id)event
{
    if (!queuedEvents) {
        queuedEvents = [[NSMutableArray alloc] init];
        [self performSelector:@selector(processQueuedEvents) withObject:nil afterDelay:2.0];
    }
    [queuedEvents addObject:event];
}

- (void)processQueuedEvents
{
    NSArray *events = queuedEvents;
    queuedEvents = nil;
    for (id event in events) {
        // Do something with event
    }
    [events release];
}

This will buffer up to two seconds of events before processing them.

rpetrich
  • 32,196
  • 6
  • 66
  • 89
1

queuing and execution of tasks became very easy with GCD and block.

-(void) delayHandler:(NSNotification *)notif {
   // check if notif is already on the event queue
   for (NSNotification *note in self.eventqueue) {
       if (note == notif) // use proper compare function, this is just a pointer compare
          return;
   }
   [self.eventqueue addObject:notif];

   dispatch_queue_t queue = self.handlerQueue;
   dispatch_time_t delay;delay = dispatch_time(DISPATCH_TIME_NOW, 50000 /* 50μs */);
   dispatch_after(delay, queue, ^{ 
      //event handling code here
      for (NSNotification *note in self.eventqueue) {
         // handle note in any way
         [self.eventqueue removeObject note];
      }
   );
}

Just a general idea.

Sean S Lee
  • 1,274
  • 9
  • 24
0

I don't quite understand your question, but I assume that you want to run a filter on events.

There's a pretty straightforward approach to this with NSNotificationCenter. We'll be posting a notification, then setting some parameters on the userInfo property of NSNotification, then running the notification through a filter.

First, in the .m of the file that will be sending the notification, where the notification should be sent:

NSNotification *notification = [NSNotification notificationWithName:@"notification" 
                                                             object:self
                                                           userInfo:[NSDictionary dictionaryWithObject:someFilterObject
                                                                                                forKey:@"object"]];
[[NSNotificationCenter defaultCenter] postNotification:notification];

Now, in the .m of the file that will be reading the notification:

In init (or some initializer):

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(filterNotification:)
                                             name:@"notification"
                                           object:/* the object that sends the notification */];

In filterNotification:(NSNotification *)notification:

if ([[notification.userInfo objectForKey:@"object"] isEqual:desiredObject]) {
    // Do something
} else {
    return;
}
aopsfan
  • 2,451
  • 19
  • 30
0

I have not looked into why you might be getting multiple EKEventStoreChangedNotification events, and I don't know if that is normal—but my first though when I read that you just wanted to delay processing by 2 seconds was that sounded like a kludge and there must be a better way to solve this.

Reading the Apple documentation it suggests that if you do not wish to update unless it is absolutely necessary to do so, you should call a refresh and only if this is YES would you then release and reload all EKEvent objects that you were retaining. It seems to me that this would ensure that duplicate EKEventStoreChangedNotification messages would not result in your code running multiple times, providing your code in response to this is processed on the thread that is receiving the notifications (so subsequent notifications are not being received while the code in response to the initial one is still running).

Duncan Babbage
  • 19,972
  • 4
  • 56
  • 93
  • thanks Duncan - was also covering off events like if another app updates the calendar whilst my app is in the background and comes to foreground, or could be something like a calendar sync to google too – Greg Oct 22 '11 at 21:44