Overview

Handling iOS app states with a state machine

No Comments

Developers tend to neglect the importance of states an app can be in. Application states make an important part of the app lifecycle, and they should be addressed properly. This article addresses this problem. Additionally, a demo project is provided demonstrating what is referred to in the following article.

So, to recap, an iOS app can be in one of the five different states:

  • Not running
  • Active
  • Inactive
  • Background
  • Suspended

The state diagram below describes transitions between the states.

ios-states-AppState-Apple

As an indication that the app is about to transition to a different state, there are different callbacks available in UIApplicationDelegate:

  • applicationWillTerminate
  • applicationWillResignActive
  • applicationWillBecomeActive
  • applicationDidEnterBackground

There is no callback when the app is transiting from background to suspended state or from suspended to terminated state.

Depending on your app’s complexity, handling each state can be quite complex. That makes AppDelegate class complex, which in turn, makes testing also complex.

How to deal with this problem? The answer can be quite simple: a state machine. In its official documentation regarding handling app states, Apple mentions the word ‘state’ 48 times, and not once did I see someone applying the state machine concept to this area.

For apps that have a complex handling of different states, state patterns can make your life easier.

So what are the apps that actually do have complex state handling? Some examples are apps featuring:

  • games – dropping frames and pausing whole game when going to background, resuming game when coming back from background
  • voip – handling incoming call in background, continuing call while in background, answering call while in background…
  • security – blurring some sensitive content on the screen before app enters background and iOS captures screen for multi-task purpose
  • background tasks – scheduling any work to be done in background

The state machine

So, how would a state diagram solution look for an iOS app? On an image below is a proposed state diagram.

ios-states-AppStates

You might expect 5 states on the diagram, but there are actually 7, all explained below the AppDelegate code sample:

class AppDelegate: UIResponder, UIApplicationDelegate {
 
    var window: UIWindow?
 
    var _currentState: AppState = AppStateInit()
    var currentState: AppState {
        get {
            return _currentState
        }
        set(newVal) {
            _currentState.leaveState()
            _currentState = newVal
            _currentState.enterState()
        }
    }
 
    func application(application: UIApplication, willFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
        return (currentState as! AppStateInit).willFinishLaunchingWithOptions()
    }
 
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
        return (currentState as! AppStateInit).didFinishLaunchingWithOptions()
    }
 
    func applicationWillResignActive(application: UIApplication) {
        currentState = AppStateResignActive()
    }
 
    func applicationDidEnterBackground(application: UIApplication) {
        currentState = AppStateBackground()
    }
 
    func applicationWillEnterForeground(application: UIApplication) {
        currentState = AppStateWakeUp()
    }
 
    func applicationDidBecomeActive(application: UIApplication) {
        currentState = AppStateActive()
    }
 
    func applicationWillTerminate(application: UIApplication) {
        currentState = AppStateTerminating()
    }
}

Init state

This is a brief state existing while an app is launching. It does the work of willFinishLaunchingWithOptions and didFinishLaunchingWithOptions methods. Some examples of what this state is responsible for:

  • setting up the app
  • showing initial view, ie Login view if user hasn’t be logged in already, or ‘Main’ view if auto-login is supported.
  • registering for Apple Push Notifications
  • registering launch shortcuts
  • other one-time-per-app-lifetime actions

Active state

This is the state where an app is shown on the device’s screen and the app is receiving user touches. The only way to enter this state is from inactive state, except when app is launching. Then the app transits directly from Init to Active state.
In this state we usually want to start the timers which were paused when leaving the active state.

class AppStateActive: AppState {
 
    override func enterState() {
        super.enterState()
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }
}

Inactive state

Inactive state is a brief state appearing while the app is leaving or entering the active state. While inactive, the app is updating the UI but it is not receiving user touches.

The inactive state can be triggered by showing the Notifications view from the status bar, or by showing ‘Control centre’ from the bottom (see video further down).
In our model, there are two Inactive states:

  • Resign Active – app is leaving active state and entering background state
  • Wake Up – app has been in background state and is about to become active

We want to differentiate these because different actions are taken in each case. Additionally, different callbacks are called in AppDelegate to let us know about each state: application:willResignActive and application:willEnterForeground.

Resign Active state

When the app enters resign-active state we may want to do the following:

  • pause any timers that were running (ie pause game)
  • throttle down OpenGL frames
  • prepare the app for taking screenshot for multi-task menu: if the app shows sensitive or secure data, it may be good to blur that content or just show single-colored view with app logo in the centre

Wake up state

This state is like resign-active state but with a different direction: an app has been backgrounded and is about to become active. In this state we usually want to undo any changes made when entering the background. It is easy to separate this state from resign-active because iOS provides a special callback for it: application:willEnterForeground.

class AppStateInactive: AppState {
 
    override func enterState() {
        super.enterState()
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
    }
}
 
class AppStateWakeUp: AppStateInactive {
 
    override func enterState() {
        super.enterState()
        // undo any changes made when entering the background
    }
}
 
class AppStateResignActive: AppStateInactive {
 
    override func enterState() {
        super.enterState()
        // undo any changes made when entering the background
    }
}

Background state

This state is triggered by the application:didEnterBackground callback. This state usually lasts about 10 seconds unless more time was requested from the system, in which case the app gets 180 seconds of background. VoIP apps can have longer background state. This state can be used for:

  • saving any persistent data that should be saved
  • saving enough information about the app state so that it can be restored at later point, in case it gets killed by iOS
  • you may want to pause timers when this state is entered (instead of pausing them when entering Inactive (Resign Active) state). This depends on what app’s timers actually do and should UI be updated while the app is in inactive state
class AppStateBackground : AppState {
 
    override func enterState() {
        super.enterState()
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }
 
    override func leaveState() {
        super.leaveState()
        // can leave to terminated or wake-up
 
        // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
    }
}

Terminating state

This is a brief state entered just before the app is about to be killed. However, if the app was in ‘Suspended’ state, Terminating state won’t be entered. So don’t rely on this state for any important work: It will only be entered if the app was previously in the background state. In this state, you may want to save any data if it’s appropriate, but it’s better to handle that when entering Background state. This state can be useful when opting-out from the background.

class AppStateTerminating: AppState {
 
    override func enterState() {
        super.enterState()
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }
}

Below is a video demonstration of most states (the init-state and terminated are not visible)

Below is a class diagram. Notice that AppStateWakeUp and AppStateResignActive extend AppStateInactive, but that need not be done like this. If it suits you, those classes can extend abstract AppState directly.

ClassDiagram

Notifications approach

A similar goal can also be accomplished by listening to notifications:

  • UIApplicationDidEnterBackgroundNotification
  • UIApplicationWillEnterForegroundNotification
  • UIApplicationDidBecomeActiveNotification
  • UIApplicationWillResignActiveNotification
  • UIApplicationWillTerminateNotification

That way, AppDelegate doesn’t have to implement any of the ‘transition’ methods, but every state would have to listen for these changes or some kind of mediator would have to be introduced which will set the current state.

Benefits

Okay, so here are a few benefits of breaking app states into a state machine:

  • Testability – it’s usually easier to unit-test each state than testing the whole AppDelegate class
  • Happier OCLint – less complexity and LOC of the probably already overcomplicated AppDelegate class
  • better (or more clear) control of app states

Conclusion

For simple apps (most apps) you usually don’t need any of this. This approach is intended only when an app’s state handling becomes somewhat complicated. I don’t recommend abusing or forcing this design unless you actually need it. I use this approach for complex apps I’ve worked on, and it has proven good for me so far.
Don’t forget to checkout the example.

Useful links

Strategies for Handling App State Transitions
The App Life Cycle
Background Execution
Execution States for a Swift iOS App

Comment

Your email address will not be published. Required fields are marked *