Detecting the first launch of the iOS application — the wrong and the right way
By: Oleg Dreyman
Tags: Articles, Swift, iOS

Detecting the first launch of the iOS application — the wrong and the right way

Another post about “testability matters”

The concept of the “first launch” is crucial for many apps. You may want to show a quick presentation screen, populate your model layer with pre-defined data or do many other interesting things.

The actual logic behind this functionality is quite trivial: we need to persist some flag indicating that the app was launched before, and on every launch check the existence of that flag, so if it doesn’t exist, it’s our first launch.

So let’s not talk too much about that and dive right into code. The wrong one, actually. But it doesn’t matter now. So:

final class FirstLaunch {



let userDefaults: UserDefaults = .standard



let wasLaunchedBefore: Bool

var isFirstLaunch: Bool {

return !wasLaunchedBefore

}



init() {

let key = "com.any-suggestion.FirstLaunch.WasLaunchedBefore"

let wasLaunchedBefore = userDefaults.bool(forKey: key)

self.wasLaunchedBefore = wasLaunchedBefore

if !wasLaunchedBefore {

userDefaults.set(true, forKey: key)

}

}



}

And then at the call site:

let firstLaunch = FirstLaunch() if firstLaunch.isFirstLaunch {

/// do things

}

This approach works just fine. Nothing is wrong with the logic, it really detects the first launch of your application. However, there is a huge problem with this code:

How are we going to test this?

And I mean not just testing the logic (and even that’s now is quite hard even though the class does so little), but testing the code which relies on this logic as well. All our “run only at the first launch” functionality should be tested, but how are we going to do it? Deleting the app every time we made a change is not an option, obviously.

So we need to make our FirstLaunch hackable. We need it to report “first launch” when it’s actually not while we’re testing. And we need it to be production-ready as well.

So in order to do that, we have to look at what FirstLaunch actually does. We should limit the scope as much as possible. So, what are the responsibilities of FirstLaunch ?

It checks the existence of “was launched” flag and stores that in memory. If the flag is false (meaning it’s our first launch and the flag was explicitly set to false or it simply doesn’t exist), it creates the true flag and persists it.

And, actually, that’s it. That’s all our FirstLaunch has to do.

So think of that: why should our FirstLaunch bother at all about UserDefaults ? Does it really matter how exactly we retrieve and persist this “was launched” flag? It’s a simple implementation detail, after all. And FirstLaunch should be bare logic. So let’s get rid of the UserDefaults dependency, once and for all:

final class FirstLaunch {



let wasLaunchedBefore: Bool

var isFirstLaunch: Bool {

return !wasLaunchedBefore

}



init(getWasLaunchedBefore: () -> Bool,

setWasLaunchedBefore: (Bool) -> ()) {

let wasLaunchedBefore = getWasLaunchedBefore()

self.wasLaunchedBefore = wasLaunchedBefore

if !wasLaunchedBefore {

setWasLaunchedBefore(true)

}

}



convenience init(userDefaults: UserDefaults, key: String) {

self.init(getWasLaunchedBefore: { userDefaults.bool(forKey: key) },

setWasLaunchedBefore: { userDefaults.set($0, forKey: key) })

}



}

Instead of using UserDefaults , from which we needed only two functions, we now just inject exactly those two functions right into our init . And we also made a convenience initializer to make the creation of UserDefaults -based FirstLaunch easier.

let firstLaunch = FirstLaunch(userDefaults: .standard, key: "com.any-suggestion.FirstLaunch.WasLaunchedBefore") if firstLaunch.isFirstLaunch {

// do things

}

And now creating our “hacked” FirstLaunch is a trivial task:

let alwaysFirstLaunch = FirstLaunch(getWasLaunchedBefore: { return false }, setWasLaunchedBefore: { _ in }) if alwaysFirstLaunch.isFirstLaunch {

// will always execute

}

Or we could go fancy (and we should):