Solving an iOS App Processing Conundrum—All In a Day’s Work at DevBBQ

November 9, 2016 | Mobile

Here at DevBBQ, we’re digital creators with big appetites—and nothing gets our mouths watering more than when clients come to us with their product challenges.

We were recently tasked with finding a creative solution for just such a challenge, this time involving an app that one of our client distributes to its management team in order to track employee activities, store and product details (including data covering 7 years of financial metrics across 1,000+ stores), sales targets, and other business KPIs. This particular app manages a lot of data and syncs with our client’s servers on a daily basis—a 2–5 minute process. Our client recognized that their users disliked having to wait for the app to import the day’s data, and asked if this could be done in off-hours in the background—say, overnight—so that everything would be ready when users started work in the morning.

Based on this, we arrived at the following 2 client requirements:

  1. Perform a data sync at a specific time (specifically: after internal servers have updated, but before users have logged on for the day).
  2. Perform the data download and database update (duration: 2–5 minutes; fairly CPU and memory intensive).

These were clear enough from a development perspective, but from experience, we knew that we were going to have to use some finessing in order to mesh our client’s needs with Apple’s rules:

  1. Apps are allowed time for background processing (i.e., when the app first goes into the background for 10–30 seconds).
  2. Apps can respond to push notifications when in background mode and are again allowed only 10–30 seconds of processing time.
  3. Apps can process network requests in the background, which allows the device to finish a network request if the user goes into background mode during the request.
  4. Each device chooses a time when it can perform a network background fetch request. This is not under developer control, and usually takes place when the device is idle. Again the limit is around 30 seconds and the background fetch is not guaranteed to even get called during this time.
  5. Background processing can only happen in background mode (e.g., when the user switches to another app or presses the Home button), and not when the app has been terminated.
  6. Apps must stay within certain CPU and memory limitations or the app will be shut down by the device.

After conducting some internal problem-solving work, we arrived at some smart solutions to the 2 main challenges we were facing:

Challenge #1 – Perform the data sync at a specific time

Based on Apple’s app requirements for background fetch options, each device chooses its own timing for network requests (whereas our client wanted updates to take place at a specific time of their own choosing).

Solution #1 involved using a push notification server. This was the most straightforward answer to the issue of data syncing, but it came with some significant pros and cons:

Pros

  • Client control: The client would have total control over the timing when the notification and subsequent update would take place.
  • Better user experience: Silent push notification can be deployed, so the user has no indication that a notification has been received.
  • Repurposing potential: This solution could be used for additional purposes based on the client’s wishes (e.g., inter-app messaging; returning a device’s geographical location).

Cons

  • Resource intensive: The client would need to develop a push notification server that would allow them to easily group users, to set data sync times, to store user tokens and info, etc.
  • Outsourcing issues: The client can use a third-party service, which could lead to additional expense and would require review by their legal team.
  • Control still open-ended: The user would still need to allow push notifications for the app. If they disallow, the data sync wouldn’t take place.

In the end, the client decided that the cons outweighed the pros and thus, requested another option.

Back to the drawing board we went, and we arrived at Solution #2 – Using VOIP— a sound option because VOIP-based apps are allowed to query a server at a specific time interval when running in background mode.

To enable VOIP: [NOTE: Since this app was to be distributed ad hoc, I can do this; otherwise it would get rejected by the App Store.]

  1. Go to Capabilities -> Background Modes
  2. Check the box for Voice over IP
  3. Enable the timer when the app goes into background mode: In this case, every 15 minutes, I call my autoRefreshAppData method, which compares the current time with the time the app is supposed to refresh (this time is set by the client and retrieved by the app from the client's servers). The minimum time interval is 10 minutes (600 seconds).

When the app goes into the background create the timer.

- (void) applicationDidEnterBackground (UIApplication *)application
{
    // Create the timer and set the block to be executed
    [[UIApplication sharedApplication] setKeepAliveTimeout: 900 handler:
    ^{
        [self autoRefreshAppData];
    }];
}

When the app comes back to the foreground, I stop the timer.

- (void) applicationWillEnterForeground:(UIApplication *)application
{
    // Ensure the timeout is cleared, although this should happen automatically when the app comes to the foreground
    [[UIApplication sharedApplication] clearKeepAliveTimeout];
}

This solution works well; the timer is very accurate and as long as the app is in background mode, it works flawlessly. But, there’s a hitch: setKeepAliveTimeout was deprecated in iOS9 and as of iOS10, this method no longer works.

The good news: There’s a workaround! Since the client didn't need any of the new functionality of iOS10 in their app, we can link the app to an older version of iOS. This is essentially what happens if you have an app that was released with iOS9 and wasn’t updated again by the developers for iOS10.

Here’s how to link your app to an older version of iOS [in this example, I’m running the newly released XCode 8 with iOS10 and I want to link against iOS9.3]:

  1. Log into your Apple developer account and download an older version of XCode (in this case I downloaded XCode version 7.3.1)
  2. Mount the DMG and locate the SDK [it should be something link this: /Volumes/Xcode/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk]
  3. Shut down XCode.
  4. Copy this SDK to your XCode SDKs folder [it should look something like this: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/]. When you copy it, rename to iPhoneOS9.3.sdk – or whatever makes sense based on the SDK version.
  5. Edit (with PlistBuddy) the following file:
    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Info.plist

    and change the MinimumSDKVersion key to 9.3:

    > sudo /usr/libexec/PlistBuddy/Info.plist  
    > Set MinimumSDKVersion "9.3"  
    > Save
    
  6. Start XCode and now under Build Settings -> Architectures -> Base SDK. You should see a drop-down list where you can select ‘iOS 9.3.’

Challenge #2 – Background updates take longer than 30 seconds to complete

This was a particularly finicky conundrum, as no matter which route we were to take on this app build, Apple’s requirements dictate a maximum of 30 seconds to perform background data syncs—period!

But I have another trick up my sleeve, and again, this only works because the app would not be released to the App Store: One specific category of app is allowed to run in the background for more than 30 seconds—Audio Apps. I knew that if I could tell the device that this was an audio app, then it would allow me to execute in the background for the 2 to 5 minutes it takes for my client’s data to sync. Here’s how:

  • Go to Capabilities -> Background Modes and check the box for Audio, AirPlay, and Picture in Picture

Remember: My autoRefreshAppData method is called every 15 minutes, and when the current time is within the data sync time, the sync begins:

- (void) autoRefreshAppData
{
    // Ensure the current time is within the data sync time
    if (...)
    {
        // Enable background processing
        [self beginBackgroundUpdateTask];

        // Start data sync
        ...
    }
}

To end background processing:

- (void) beginBackgroundUpdateTask
{
    if (self.backgroundUpdateTask == UIBackgroundTaskInvalid)
    {
        // Start the task, this shouldn't expire since the audio will be playing
        self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:
        ^{
            [self endBackgroundUpdateTask];
        }];

        // Play audio to avoid resetting the background timer
        [self playAudio];
    }
}

- (void) endBackgroundUpdateTask
{
    // End the background task
    [[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask];

    // Invalidate the task
    self.backgroundUpdateTask = UIBackgroundTaskInvalid;

    // Stop Audio
    [self stopAudio];
}

This way, when the data sync is finished, the endBackgroundUpdateTask method will be called.

In order to allow processing for more than 30 seconds, we need to play audio. So we set a notification observer, just in case the audio in interrupted by a phone call or some other trigger:

- (void) playAudio
{
    // Receive notification if audio is interrupted
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioInterrupted:) name:AVAudioSessionInterruptionNotification object:nil];

    // Run in a separate thread
    dispatch_async(dispatch_get_main_queue(),
    ^{
        // Don't prevent other audio from playing
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];

        [[AVAudioSession sharedInstance] setActive: YES error: &error];

        // Load a sound file that is very small, with no actual sound playing
        NSURL *soundFileURL = ...

        // Start the audio player
        self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL error:&error];
        self.audioPlayer.volume = 0.01;
        self.audioPlayer.numberOfLoops = -1; //Infinite
        [self.audioPlayer prepareToPlay];
        [self.audioPlayer play];
    });
}

- (void) stopAudio
{
    // Remove audio interruption notification
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVAudioSessionInterruptionNotification object:nil];

    // Stop the audio player
    if(self.audioPlayer != nil && [self.audioPlayer isPlaying])
        [self.audioPlayer stop];
}

- (void) audioInterrupted:(NSNotification*)notification
{
    // Restart audio
    [self stopAudio];
    [self playAudio];
}

With this, Challenge #2 was solved, and we were able to meet all of the requirements put forward by our client. The app can now process for as long a period as we want—though at the same time, must still keep in mind the CPU and memory limitations that we’d discovered based on extensive testing of apps we’ve built for iOS.

The result: Our client’s app now performs its data syncs in the background at a specific time each day and users are happy that the app is fully synched by the time they start work in the morning.

Do you have a website or app conundrum that seems unsolvable? Let our product development experts sink their teeth into your build challenges—contact us today to learn how.

[Important notes: (1) This article pertains to apps that will not be released into the App Store, but instead are intended for ad hoc/private use. (2) The coding included below is a generalized solution and leaves out certain logic that the reader will have to implement, depending on their specific situation.]


We are DevBBQ.
We are digital product creators with big appetites. For Hire.

Arrow circle down@2x 6412eee90b778a7dc0698b617fc13fb6c19fdd10f53c4479172d38bc5ce4e168
Therapia white 55d4657b58374a27a9634c3cb6db71c5efd7179c0fca9530931b0a015c78fade

At-home physiotherapy social network, appointment bookings, and payment processing.

Cooked up by DevBBQ

 

Contact Us Lets chat@2x 9e08eee888b6926761c1dfe33e7c84fb2f0345e4be974f2d9e4dca9c39d24980

 

DEVBBQ INCORPORATED