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

Another post about “testability matters”

Oleg Dreyman
AnySuggestion
5 min readMar 14, 2017

--

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?

  1. It checks the existence of “was launched” flag and stores that in memory.
  2. 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):

extension FirstLaunch {

static func alwaysFirst() -> FirstLaunch {
return FirstLaunch(getWasLaunchedBefore: { return false }, setWasLaunchedBefore: { _ in })
}

}
let alwaysFirstLaunch = FirstLaunch.alwaysFirst()if alwaysFirstLaunch.isFirstLaunch {
// will always execute
}

So what did we just do? We isolated the real responsibility of FirstLaunch from the implementation details, and we made it still easy to work with from the outside. With this approach, we can simply swap the underlying storage for our flag, and we can also easily test both the FirstLaunch and our app without breaking the code.

This is, of course, not about the first launch. This is about the design of your classes and their responsibilitites. You should aim to isolate the functionality as much as possible and always think about how to test it and the code that relies on it. You can read more about the ideology behind it here.

Thanks for reading the post! Don’t hesitate to ask or suggest anything in the “responses” section below. You can also contact me on Twitter.

Hi! I’m Oleg, the author of Women’s Football 2017 and an independent iOS/watchOS developer with a huge passion for Swift. While I’m in the process of delivering my next app, you can check out my last project called “The Cleaning App” — a small utility that will help you track your cleaning routines. Thanks for your support!

Appendix 1. Switching the logic based on the environment

If you have a central place where your FirstLaunch is placed (AppDelegate, ApplicationController, whatever), you can add this at the initialization point:

#if DEBUG
self.firstLaunch = FirstLaunch.alwaysFirst()
#else
self.firstLaunch = FirstLaunch(userDefaults: .standard, key: "your-key")
#endif

This way, we treat every launch as first in debug mode, but use the real mechanism in release. So we can switch the logic without changing a single line of code. And this code is production-ready.

Appendix 2. Other approaches to this problem

Of course, this exact problem can be solved in many ways (that’s the beauty of Swift!). I find them more cumbersome and boilerplate-ish, but will list them anyway for educational purposes.

1. Subclass UserDefaults:

class FirstLaunch {

let wasLaunchedBefore: Bool
var isFirstLaunch: Bool {
return !wasLaunchedBefore
}

init(userDefaults: UserDefaults, key: String) {
let wasLaunchedBefore = userDefaults.bool(forKey: key)
self.wasLaunchedBefore = wasLaunchedBefore
if !wasLaunchedBefore {
userDefaults.set(true, forKey: key)
}
}

}
class AlwaysFalseUserDefaults : UserDefaults {

override func bool(forKey defaultName: String) -> Bool {
return false
}

override func setValue(_ value: Any?, forKey key: String) {
// do nothing
}

}
let alwaysFalseUserDefaults = AlwaysFalseUserDefaults()
let alwaysFirstLaunch = FirstLaunch(userDefaults: alwaysFalseUserDefaults, key: "")

2. Subclass FirstLaunch:

class AlwaysFirstLaunch : FirstLaunch {

override var isFirstLaunch: Bool {
return true
}

}
let alwaysFirstLaunch = AlwaysFirstLaunch(userDefaults: UserDefaults(), key: "")

This is actually a really bad solution since we can override only getter for isFirstLaunch, not for wasLaunchedBefore. And it’s still writing right to the UserDefaults.

3. Protocols!

protocol FirstLaunchDataSource {

func getWasLaunchedBefore() -> Bool
func setWasLaunchedBefore(_ wasLaunchedBefore: Bool)

}
class FirstLaunch {

let wasLaunchedBefore: Bool
var isFirstLaunch: Bool {
return !wasLaunchedBefore
}

init(source: FirstLaunchDataSource) {
let wasLaunchedBefore = source.getWasLaunchedBefore()
self.wasLaunchedBefore = wasLaunchedBefore
if !wasLaunchedBefore {
source.setWasLaunchedBefore(true)
}
}

}
struct AlwaysFirstLaunchDataSource : FirstLaunchDataSource {

func getWasLaunchedBefore() -> Bool {
return false
}

func setWasLaunchedBefore(_ wasLaunchedBefore: Bool) {
// do nothing
}

}
let alwaysFirstLaunchSource = AlwaysFirstLaunchDataSource()
let alwaysFirstLaunch = FirstLaunch(source: alwaysFirstLaunchSource)

Although many will prefer this solution as the most “swifty” one, I strongly doubt that. Using protocol here is an overkill and doesn’t give much benefits. For example, we cannot simply conform UserDefaults to FirstLaunchDataSource (where are we gonna get the key?), so we have to create something like that:

struct UserDefaultsFirstLaunchDataSource : FirstLaunchDataSource {

let defaults: UserDefaults
let key: String

func getWasLaunchedBefore() -> Bool {
return defaults.bool(forKey: key)
}

func setWasLaunchedBefore(_ wasLaunchedBefore: Bool) {
defaults.set(wasLaunchedBefore, forKey: key)
}

}
let source = UserDefaultsFirstLaunchDataSource(defaults: .standard, key: "your-key")
let firstLaunch = FirstLaunch(source: source)

Kind of ugly, don’t you think? As for me, passing two closures is much, much easier

--

--

Oleg Dreyman
AnySuggestion

iOS development know-it-all. Talk to me about Swift, coffee, photography & motorsports