Don't Be Slack

Have you ever opened a slow app, and lost your patience waiting for it to load, and closed it out of frustration? Well, lets make sure that your app isn't like that. And to get to that point, i'd like to talk about the foundations of your app. Because if the foundations are right, you'll get "fast" for free.

Configuration Screen

You'll likely want some configuration to drive your app. Everyone seems to want remote config like this, to turn features on/off, or have an endpoint manifest - i've seen that a couple times, that kind of thing.

So the first time your app runs, you'll have to load this config. I recommend re-using the launch screen storyboard for this. Add a 'ViewControllers' folder, create the class, and edit the class name in the launch storyboard. Careful not to connect any outlets, or it won't be eligible to be displayed as a launch screen any more. To get around that, in the viewDidLoad, add some code to add an activity indicator, and set myIndicator.center in the viewDidLayoutSubviews.

Models

Next add a models folder, and add the config model object. I keep all my models as dumb as possible, they only have properties, and a convenience initialiser for parsing from JSON that your API provides. I also implement NSCoding on ones that are to be cached - such as the config model.

Services

I always add a 'Services' folder to any new app. This is where all the classes/structs that talk to your backend go. I make these as all static methods, with no state to be kept in these. Any state goes into managers as described below. These services are to be as close to pure functions as possible. So you'll need the first service, being the one that fetches the config.

Nowadays in Swift, my services all call back with Result Enums with Success/Error, with success storing the model object/s in an associated value and fail storing an NSError. This makes it crystal clear whether a service succeeded or failed, and it removes the need for optionals. It looks like so:

enum Result<T> {
    case Success(T)
    case Error(NSError)
}

struct ConfigService {
    typealias Completion = Result<Config> -> Void
    static func requestConfig(completion: Completion) {
        // do the HTTP request, calling completion with either
        // a Success or Error enum.
    }
}

struct Config {
    let apiVersion: Int
    let foo: String
}

Manager

I don't particularly like it when my view controllers talk to my services. I prefer them to talk to managers, and only the managers talk to services. So Managers is the next folder I add to my project. Managers are singletons, and they maintain state. Preferably as little state as possible, but you have to be realistic. My first manager is the config manager. This is responsible for fetching config remotely, caching it, keeping state of the current config, and sending a notification if config changes.

App Delegate

Now depending on your app requirements, you may need to re-fetch config every time the app loads, or you might be OK to cache it. Hopefully you're ok to cache it, because we want the app to be fast, and every HTTP roundtrip at startup slows your app up, and every increased delay bleeds users. So from now on I'll assume you're OK to cache your config.

The app delegate needs to choose which view controller your app should start on. It needs to ask the config manager 'do you have a cached config?'. If so, it starts your app on the dashboard, otherwise it starts on the config screen. This choice of view controllers needs to happen in the app delegate's appDidFinishLaunching because that's the only method to avoid an ugly initial flash between the launch screen and the loading screen which I see in some apps (Twitter most frustratingly). It will be seamless because after all they are based on the same storyboard, as mentioned above in the 'configuration screen' paragraph.

If your config is loaded, the config manager is responsible for re-fetching the config in the background, 10 seconds after app launch, once things have settled down. If the config is the same (as it should be, 99% of the time), it will do nothing - you got the benefit of a faster app launch and all is good. If the config changes, it saves the new config to the cache, and you have some options:

  • You can set a flag so that next time the app backgrounds, it aborts, so that next foreground the new cache gets picked up. This is simple and will hopefully be good enough - the user probably won't even notice your app aborting. They'll use the stale config until they background your app though - can you live with that? Often yes.
  • Or you can design any part of the app that uses config to listen to 'cache updated' notification and work with that. This can get complicated.
  • Or you can make your app not cache its config, and fetch it every time the app launches. But this just makes your app one HTTP roundtrip slower every launch. This is simple but slow.

Navigation

You want to put some thought into your app's view controller hierarchy. Keep it simple, because simple means fast. Here are some suggestions:

  • Your UIWindow's rootViewController is a UINav with the nav bar hidden. This root VC never changes.
  • The app delegate decides if the UINav's initial root VC is either the config loading screen, or your dashboard.
  • Your dashboard is like the home screen of the app. It might actually be a UITabBarController or something similar, depending on your UX.
  • Here is the brilliant part of having a UINav as the root VC: When your config finishes loading, you can call 'setViewControllers' on the UINav, passing in an array that contains only a dashboard vc, and it'll transition from the config loading to the dashboard, and the dashboard will become the UINav's new root vc. And your config vc will be discarded. This technique of discarding 'from' view controllers is even more useful later if you have a logon flow.
  • And of course you'll probably want to make a custom view controller transition to go from the config to the dashboard. A crossfade will probably do the trick.

Dashboard

Your dashboard is the home screen of your app. If your UX is modern (eg avoiding hamburger menus), it'll be a UITabBarController or some similar custom containment view controller that you whip up. Or it could simply be a plain UIViewController.

The trick to making your app fast is: HTTP roundtrips on mobile are slow, so only use ONE.

It helps to get your app to the point where you only need 1 request before your app is showing useful data to the user. And even better if you can cache some stuff to show while that 1 request is loading. You'll really have to think about your particular app to see what makes sense.

You'll probably really have to poke at your backend team to get that initial single useful request. But if you can get it, then you've got the app startup holy grail: A single HTTP request, and the app delegate starting the app on the dashboard view controller from the get-go (assuming config is cached).

Thanks for reading - now go, please make your apps fast!

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.