Paul Roub

A Software Tool Geek in His Natural Habitat

Zumero: Background Sync in Objective-C

Mobile offline RSS reader, Part 5

In the second “crossover episode” of our series (previous episodes begin on Eric’s blog), we look at background operations.

Previously, we built Zumero databases full of RSS lists and feed contents. The ZumeroReader sample app pulls, reads and displays those feeds on iOS devices. In action, it looks like this:

At startup, it pulls the feed list like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ZumeroDB *db = [[ZumeroDB alloc]
              initWithName:@"all_feeds"
              folder:path host:host];
db.delegate = self;

NSError *err = nil;
BOOL ok = YES;

if (! [db exists])
  ok = [db createDB:&err];

ok = ok && ([db isOpen] || [db open:&err]);

NSDictionary *scheme = nil;
NSString *uname = nil;
NSString *pwd = nil;

ok = ok && [db sync:scheme user:uname password:pwd error:&err];

We’re creating a Zumero database file named “all_feeds”. It will sync with a similarly-named dbfile on the server side.

We don’t need any authentication info, since (as previously discussed) the feed lists and feed databases are pullable by anyone.

The ZumeroDB::sync method always performs its network activity on a background thread, calling delegate methods when the action is completed (hence db.delegate = self).

In this case, on sync success, we grab the latest copies of the individual feed databases:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ok = [db selectSql:@"select feeds.feedid, url, title from feeds, about "
    "where (feeds.feedid = about.feedid)" values:nil
    rows:&rows error:&err];
// ...
  
for (NSDictionary *row in rows)
{
  NSNumber *id = [row objectForKey:@"feedid"];
  
  [self syncFeed:id];
}

// ...

- (void) syncFeed:(NSNumber *)feedid
{
  // ...
  NSString *dbname = [NSString stringWithFormat:@"feed_%@", feedid];
  ZumeroDB *db = [[ZumeroDB alloc] initWithName:dbname folder:path host:host];
  // ...
  db.delegate = self;
  ok = [db sync:nil user:nil password:nil error:&err];
  // ...
}

But what if feeds are updated while the app is running? There are seemingly infinite strategies for launching background tasks in iOS, and you’ll use the one that fits your application best. In this case, I cribbed a simple plan from a StackOverflow post - every time a touch is detected, we restart a timer. If that timer actually manages to expire, then we’ve seen no touches in 5 seconds.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)sendEvent:(UIEvent *)event {
    [super sendEvent:event];
  
    NSSet *allTouches = [event allTouches];
    if ([allTouches count] > 0) {
        UITouchPhase phase = ((UITouch *)[allTouches anyObject]).phase;
        if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded)
            [self resetIdleTimer:maxIdleTime];
    }
}

- (void)resetIdleTimer:(NSTimeInterval)secs
{
    if (idleTimer) {
        [idleTimer invalidate];
      idleTimer = nil;
    }
  
    idleTimer = [NSTimer scheduledTimerWithTimeInterval:secs
               target:self selector:@selector(idleTimerExceeded)
               userInfo:nil repeats:NO];
}

When that timer fires, we check to see if its been 5 minutes since our last sync, and that a sync is wanted. If so, we sync again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (void)idleTimerExceeded {
  if (wantToSync)
  {
      NSTimeInterval since = [nextSync timeIntervalSinceNow];
      
      if (since <= 0)
      {
          wantToSync = FALSE;
          
          BOOL ok = FALSE;

          // the view controller's sync method from earlier          
          if (mvc)
              ok = [mvc sync];
          
          if (ok)
              self.networkActivityIndicatorVisible = YES;
          else
              // the sync call failed; try again later
              [self waitForSync:(10 * 60)];
      }
      else
      {
          // nope, check again next idle time
          [self resetIdleTimer:since];
      }
      
      return;
  }
  
  [self resetIdleTimer:maxIdleTime];
}

We also kill our timers when exiting, restart them when waking up or activating, etc.

Finally, when we go into the background, we try for one last sync before our process is suspended:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)applicationDidEnterBackground:(UIApplication *)application
{
  // ...
  bgtask = [application beginBackgroundTaskWithExpirationHandler:^{
        [self endBackgroundTask:bgtask];
        bgtask = UIBackgroundTaskInvalid;
    }];
    dispatch_async(dispatch_get_global_queue(
      DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      [mvc sync];
      // finishBackgroundTask will be called by the sync handlers
    });
}

Like any good sample app, there’s so much that this app doesn’t do. You’re invited to experiment with adding them.

  1. We don’t maintain any read/unread information, either at the feed level or for individual items. This would be a good place to try creating/syncing new databases.
  2. We na├»vely grab the first <link> element we see and assume that it’s our permalink, without ever checking its rel attribute or content type.
  3. A feed’s contents are displayed as one big chunk of HTML. Individual table cells might be nice.
  4. We don’t make any attempt to become a long-running background process, since we don’t actually play audio, collect location information, etc. Might be fun to play with that.
  5. Configuring the app is done by editing the source and rebuilding. That seems rude.
  6. If you did update some sort of last-read database, wouldn’t that be a great moment to kick off a background sync? (The Wiki app included in the Zumero SDK does this when a page is created or saved)

Comments