25/03/2018
Detecting the first launch of the iOS application — the wrong and the right way
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):
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!