After so many years in iOS development, I realized that I have been bootstrapping new projects wrong from the beginning. As many of the samples you can find online (including 3'rd party SDK integration samples), I too was following this convention to launch my apps;
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Initalize data-structures, prepare for notifications, check launchOptions for routing if available
window = UIWindow(frame: UIScreen.main.bounds)
window?.tintColor = R.color.purple()
window?.backgroundColor = R.color.cloudBlue()
window?.rootViewController = UIViewController()
window?.makeKeyAndVisible()
return true
}
What can go wrong with this? you can find similar samples online where you ditch storyboards and go hero with layout constraints, right? For me, a lot of things did go wrong. Especially if you’re putting content-available
in your notifications. AKA: Silent notifications for background fetching.
Apple’s developer documentation defines silent notifications as;
To deliver a background notification, the system wakes your app in the background. On iOS it then calls your app delegate’sapplication(_:didReceiveRemoteNotification:fetchCompletionHandler:)
method. On watchOS, it calls your extension delegate’s didReceiveRemoteNotification(_:fetchCompletionHandler:) method. Your app has 30 seconds to perform any tasks and call the provided completion handler
An elegant explanation, but one might easily overlook the detail the system wakes your app in the background
. This means that if your app is in an unattached (killed) state your app will be woken up upon receiving a silent notification, and it will call the following delegate methods of AppDelegate;
- willFinishLaunchingWithOptions
- didFinishLaunchingWithOptions
Let’s try this on debugger via setting “Launch” type from “Automatically” to “Wait for the executable to be launched” and set a breakpoint to first line of `didFinishLaunchingWithOptions`
That’s the point where everything goes wrong in my case. I initialize my coordinator and a ViewController responsible for fetching API resources and also sending analytic events and attributes. I highly depend on silent notifications to push minor updates, especially for fetching assets. But that creates a problem in calculating daily active user charts, because at that point analytics tools already logged their first events, especially session events.
Accepting the reality that these are not really active users, but faulty app initialization took some courage
Solution
Simmer down for a second if you have the same problem, embrace the fault, and take a look for Apple’s developer documentation on “Responding to the Launch of Your App”
On “didFinishLaunchingWithOptions”, articles state that you should initialize your data structures, verify required resources, prepare notifications, check for launch options for routing, and perform any one-time setups such as initializing 3'rd party SDKs. Also warns you not to run long tasks here. But doesn’t mention anything about the UI.
Let us roll back to the main article and check out the overall lifecycle. There is a great link to “Preparing Your UI to Run in the Foreground”. It explains when you should prepare your UI for both SceneDelegate and AppDelegate approaches. So Apple suggests we prepare the UI in “applicationDidBecomeActive” method. This method is called when your application transitions from killed or background state to foreground state. So I ended up changing how I launch my app to this;
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: cUIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Initalize data-structures, prepare for notifications, check launchOptions for routing if available
window = UIWindow(frame: UIScreen.main.bounds)
window?.tintColor = R.color.purple()
window?.backgroundColor = R.color.cloudBlueStart()
return true
}
func applicationDidBecomeActive(_ application: UIApplication) {
if window?.rootViewController == nil {
window?.rootViewController = UIViewController()
window?.makeKeyAndVisible()
}
}
Silent notifications no more trigger my coordinator like a regular launch, and doing this also consumes fewer system resources where you really need a background fetch. Hope you find it helpful, any comments or questions are welcome