Refactoring AppDelegate: Push Notifications Edition

Photo by Hal Gatewood on Unsplash

Discover the enhanced version of the article, complete with refined syntax highlighting and all the latest updates, now available on my personal blog! Your feedback and engagement are greatly appreciated and help fuel future content:

When we start out with our iOS development journey and work on our first big project, we often make the mistake of adding all the application level code in a single file, AppDelegate, or at least I did *sad emoji*

Let’s say we want to pull out our Push Notification methods and it’s implementation from the AppDelegate and add it in a separate file (same can go for Firebase initialisation, Location services, App basic checks/initialisation such as Documents directory management, Jail Broken device checks etc), we can do so by following a design pattern called as Factory Pattern. We can use a lot of other strategies available but I personally find this one somewhat simple to start with as other involves somewhat complicated design strategy.

Before be begin, this article will primarily focus on Push Notifications and it’s delegate methods but the same can be used for any of the implementation/s specified before

With that thing cleared up:

Let us begin !!!

First, let’s look at this OG way of writing AppDelegate.swift

import UIKit
import UserNotifications

@UIApplicationMain

class AppDelegateNew: UIResponder, UIApplicationDelegate {

// MARK: Stored properties
var window: UIWindow?

private let notificationCenter = UNUserNotificationCenter.current()

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Register device for receiving Push Notifications
registerForPushNotifications()

return true
}
}
// MARK: Application Lifecycle methods
extension AppDelegateNew {
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
}
}
// MARK: Notification methods
extension AppDelegateNew: UNUserNotificationCenterDelegate {
/// Requests the user for Push Notification access
private func registerForPushNotifications() {
// Setting delegate to listen to events
notificationCenter.delegate = self
/// Perform some notification registration magic !!!
// Requesting access from user
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
// Checking if access is granted
guard granted else { return }
// Registering for remote notifications
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
/// We receive the device token that we requested
///
/// We save the device token to our local storage of choice
///
/// (If needed) We register the device token with our FCM
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
/// Some error occured while registering for device token
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
/// 1. Called when the user taps on a notification and the app is opened
/// 2. Responds to the custom actions linked with the notifications (like categories and actions used for notifications)
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
/// Called when the app is in foreground and the notification arrives
}
}

Here, as usual, we have written our Push Notification initialisation as well as delegate methods inside the same file, which does not looks like a problem, right now, but as the AppDelegate code increases with more and more initialisations such as Firebase, Location Services, Basic Initialisation checks, AppDelegate becomes huge with ever-growing number of lines of code and all the functionalities cluttered at the same place

To resolve this, let’s break AppDelegate up!

Since we’re breaking up our AppDelegate, so each initialisation that is indirectly part of AppDelegate, will have to conform to a type that AppDelegate conforms to, namely UIResponder and UIApplicationDelegate

typealias AppDelegateType = UIResponder & UIApplicationDelegate

Let’s extract all our Push Notification methods into it’s own file

import UIKit

final class PushNotificationDelegate: AppDelegateType, UNUserNotificationCenterDelegate {

static let shared = PushNotificationDelegate()

/// Used to avoid unnecessary object intialisation
private override init() { }

private let notificationCenter = UNUserNotificationCenter.current()

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

registerForPushNotifications()
return true
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {

/// We receive the device token that we requested
///
/// We save the device token to our local storage of choice
///
/// (If needed) We register the device token with our FCM
}

func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
/// Some error occured while registering for device token
}

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

/// 1. Called when the user taps on a notification and the app is opened
/// 2. Responds to the custom actions linked with the notifications (like categories and actions used for notifications)
}

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

/// Called when the app is in foreground and the notification arrives
}

}

// MARK: - Helper Methods
extension PushNotificationDelegate {

/// Requests the user for Push Notification access
private func registerForPushNotifications() {

// Setting delegate to listen to events
notificationCenter.delegate = self

/// Perform some notification registration magic !!!

// Requesting access from user
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in

// Checking if access is granted
guard granted else { return }

// Registering for remote notifications
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}

}

}

We will implement only those methods of app lifecycle (such as didFinishLaunchingWithOptions, applicationDidBecomeActive) which we need for each implementation and let go of the rest which we do not need

Just like PushNotificationDelegate, we can have other files/class for each implementation as follows

final class FirebaseAppDelegate: AppDelegateType {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Perform firebase setup configurations
return true
}
}

final class CoreDataAppDelegate: AppDelegateType {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Setup core data and it's dependencies
return true
}
}

final class BackgroundFetchAppDelegate: AppDelegateType {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Setup background fetch mechanism
return true
}
}

final class IndependentTasksAppDelegate: AppDelegateType {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Setup individual tasks like directory creation, Jailbroken device check
return true
}
}

Now that our PushNotificationsDelegate is ready, we need a mechanism to attach it to our main AppDelegate. For this, we use Factory pattern.

But before that, we will create a Composite delegate file that will have the responsibility of calling the lifecycle methods of the app for each type of delegate implementation (PushNotificationDelegate, FirebaseAppDelegate etc) and let’s call it CompositeAppDelegate

import UIKit

final class CompositeAppDelegate: AppDelegateType {

private let appDelegates: [AppDelegateType]

internal init(appDelegates: [AppDelegateType]) {
self.appDelegates = appDelegates
}

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

/// We are handling here a case when, in worst case scenario, `didFinishLaunchingWithOptions` returns `false` like in case of a jailbroken device

// Indicates whether the app launch was succesful
var shouldTheAppLaunch = true

appDelegates.forEach { specificAppDelegate in

guard specificAppDelegate.application?(application, didFinishLaunchingWithOptions: launchOptions) ?? false else {

// Disabling the app launch because some delegate return false for the launch of the app
shouldTheAppLaunch = false
return
}

}

return shouldTheAppLaunch
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {

appDelegates.forEach { $0.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) }
}

}

// MARK: - Lifecycle methods
extension CompositeAppDelegate {

func applicationWillResignActive(_ application: UIApplication) {

appDelegates.forEach { $0.applicationWillResignActive?(application) }
}

func applicationDidEnterBackground(_ application: UIApplication) {

appDelegates.forEach { $0.applicationDidEnterBackground?(application) }
}

func applicationWillEnterForeground(_ application: UIApplication) {

appDelegates.forEach { $0.applicationWillEnterForeground?(application) }
}

func applicationDidBecomeActive(_ application: UIApplication) {

appDelegates.forEach { $0.applicationDidBecomeActive?(application) }
}

func applicationWillTerminate(_ application: UIApplication) {

appDelegates.forEach { $0.applicationWillTerminate?(application) }
}

}

Finally, our last piece of AppDelelgate refactoring puzzle is a Factory pattern. For this we will use an enum(AppDelegateFactory) and we will add all the delegate implementation’s instance to it

import Foundation

enum AppDelegateFactory {

static var fetchDelegates: AppDelegateType {

CompositeAppDelegate(appDelegates: [
PushNotificationDelegate.shared
])

}

}

Finally our App Delegate refactoring is complete !!!

Oh wait, we forgot our main AppDelegate itself

Since now, all our delegate/s implementation and it’s integration has been taken care of (via individual AppDelegates and CompositeAppDelegate), we just need a way to initialise our AppDelegateFactory which will in turn initialise CompositeAppDelegate and call the required app lifecycle methods

So here’s how our refactored AppDelegate will look like

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

// MARK: - Stored properties

var window: UIWindow?

/// Holds the app delegates
private let appDelegate = AppDelegateFactory.fetchDelegates


func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
appDelegate.application?(application, didFinishLaunchingWithOptions: launchOptions) ?? false
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
appDelegate.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}

}

// MARK: - Application Lifecycle methods
extension AppDelegate {

func applicationWillResignActive(_ application: UIApplication) {
appDelegate.applicationWillResignActive?(application)
}

func applicationDidEnterBackground(_ application: UIApplication) {
appDelegate.applicationDidEnterBackground?(application)
}

func applicationWillEnterForeground(_ application: UIApplication) {
appDelegate.applicationWillEnterForeground?(application)
}

func applicationDidBecomeActive(_ application: UIApplication) {
appDelegate.applicationDidBecomeActive?(application)
}

func applicationWillTerminate(_ application: UIApplication) {
appDelegate.applicationWillTerminate?(application)
}

}

// MARK: - URL Schemes

extension AppDelegate {

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
appDelegate.application?(app, open: url, options: options) ?? false
}

}

// MARK: - Background fetch delegate

extension AppDelegate {

func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
appDelegate.application?(application, performFetchWithCompletionHandler: completionHandler)
}

}

Doesn’t it looks nice? And the best part: if we need to add a new type of AppDelegate implementation (such as Firebase), we just need to remember 2 things

  1. Add new file (and class) for that AppDelegate implementation type and implement the lifecycle methods
  2. Add it’s instance to AppDelegateFactory

We don’t need to make any changes (in most, if not all cases) to AppDelegate or CompositeAppDelegate

The result of this refactoring is a lot less cluttered code and each delegate implementation has it own separate space (file) to add things to which increases readability and makes debugging easier

That’s it, folks! Happy Coding!

Content Inspiration:

You can connect with me on LinkedIn 👱🏻 or can get in touch with me via other channels 📬

--

--