Many techniques have been tried to tame the AppDelegate
beast, usually ending up in moving code into private functions or extensions. However, the AppDelegate
is much more complex than just moving code around. In this post, let’s examine a pluggable service technique inspired by MartÃn Ortiz with a few bonuses at the end.
AppDelegate: With Great Power Comes Great Responsibilit(ies)
Being the entry point for the app, everyone wants in on the action. From push notifications, deep links, theme setup, logger initialization.. the list goes on. Not to mention all the 3rd party SDK’s who want to plug into your app lifecycle too. Eventually your AppDelegate
looks like this:
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { Log(info: "AppDelegate.didFinishLaunchingSite started.") application.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum) UNUserNotificationCenter.current().register( delegate: self, actions: [UNNotificationAction(identifier: "favorite", title: .localized(.favorite))] ) // Initialize Google Analytics if !AppGlobal.userDefaults[.googleAnalyticsID].isEmpty { GAI.sharedInstance().tracker( withTrackingId: AppGlobal.userDefaults[.googleAnalyticsID]) } // Declare data format from remote REST API JSON.dateFormatter.dateFormat = ZamzamConstants.DateTime.JSON_FORMAT // Initialize components AppLogger.shared.setUp() AppData.shared.setUp() // Select home tab (window?.rootViewController as? UITabBarController)?.selectedIndex = 2 setupTheme() Log(info: "App finished launching.") // Handle shortcut launch if let shortcutItem = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem { performActionForShortcutItem(application, shortcutItem: shortcutItem) return false } return true } func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let webpageURL = userActivity.webpageURL else { return false } Log(info: "AppDelegate.continueUserActivity for URL: \(webpageURL.absoluteString).") return navigateByURL(webpageURL) } func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { Log(info: "AppDelegate.performFetch started.") scheduleUserNotifications(completionHandler: completionHandler) } func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { window?.rootViewController?.dismiss(animated: false, completion: nil) guard let tabController = window?.rootViewController as? UITabBarController else { completionHandler?(false); return } switch shortcutItem.type { case "favorites": tabController.selectedIndex = 0 case "search": tabController.selectedIndex = 3 case "contact": guard let url = URL(string: "mailto:\(AppGlobal.userDefaults[.email])") else { break } UIApplication.shared.open(url) default: break } completionHandler?(true) } } // MARK: - User Notification Delegate extension AppDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { guard let id = response.notification.request.content.userInfo["id"] as? Int, let link = response.notification.request.content.userInfo["link"] as? String, let url = try? link.asURL() else { return } switch response.actionIdentifier { case UNNotificationDefaultActionIdentifier: _ = navigateByURL(url) case "favorite": PostService().addFavorite(id) case "share": _ = navigateByURL(url) default: break } completionHandler() } private func scheduleUserNotifications(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { // Get latest posts from server // Persist network manager instance to ensure lifespan is not interrupted urlSessionManager = PostService().updateFromRemote { guard case .success(let results) = $0 else { return completionHandler(.failed) } guard let id = results.created.first, let post = (try? Realm())?.object(ofType: Post.self, forPrimaryKey: id) else { return completionHandler(.noData) } var attachments = [UNNotificationAttachment]() // Completion process on exit func deferred() { // Launch notification UNUserNotificationCenter.current().add( body: post.previewContent, title: post.title, attachments: attachments, timeInterval: 5, userInfo: [ "id": post.id, "link": post.link ], completion: { guard $0 == nil else { return Log(error: "Could not schedule the notification for the post: \($0.debugDescription).") } Log(debug: "Scheduled notification for post during background fetch.") } ) completionHandler(.newData) } // Get remote media to attach to notification guard let link = post.media?.thumbnailLink else { return deferred() } let thread = Thread.current UNNotificationAttachment.download(from: link) { defer { thread.async { deferred() } } guard $0.isSuccess, let attachment = $0.value else { return Log(error: "Could not download the post thumbnail (\(link)): \($0.error.debugDescription).") } // Store attachment to schedule notification later attachments.append(attachment) } } } } // MARK: - Internal functions private extension AppDelegate { func setupTheme() { window?.tintColor = UIColor(rgb: AppGlobal.userDefaults[.tintColor]) if !AppGlobal.userDefaults[.titleColor].isEmpty { UINavigationBar.appearance().titleTextAttributes = [ NSAttributedStringKey.foregroundColor: UIColor(rgb: AppGlobal.userDefaults[.titleColor]) ] } // Configure tab bar if let controller = window?.rootViewController as? UITabBarController { controller.tabBar.items?.get(1)?.image = UIImage(named: "top-charts", inBundle: AppConstants.bundle) controller.tabBar.items?.get(1)?.selectedImage = UIImage(named: "top-charts-filled", inBundle: AppConstants.bundle) controller.tabBar.items?.get(2)?.image = UIImage(named: "explore", inBundle: AppConstants.bundle) controller.tabBar.items?.get(2)?.selectedImage = UIImage(named: "explore-filled", inBundle: AppConstants.bundle) if !AppGlobal.userDefaults[.tabTitleColor].isEmpty { UITabBarItem.appearance().setTitleTextAttributes([ NSAttributedStringKey.foregroundColor: UIColor(rgb: AppGlobal.userDefaults[.tabTitleColor]) ], for: .selected) } } // Configure dark mode if applicable if AppGlobal.userDefaults[.darkMode] { UINavigationBar.appearance().barStyle = .black UITabBar.appearance().barStyle = .black UICollectionView.appearance().backgroundColor = .black UITableView.appearance().backgroundColor = .black UITableViewCell.appearance().backgroundColor = .clear } } }
This would be a nightmare to maintain. Fortunately, there’s a better way! It involves shifting the responsibilities to pluggable services.
Application Services to the Rescue!
Instead of dumping all the responsibilities on AppDelegate
, let’s create an ApplicationService
protocol that will plug into the app life cycle. Eventually your app delegate will look this:
@UIApplicationMain class AppDelegate: PluggableApplicationDelegate { override func services() -> [ApplicationService] { return [ ErrorApplicationService(), LoggerApplicationService(), AnalyticsApplicationService(), BootApplicationService(with: window), ShortcutApplicationService(), NotificationApplicationService(), ThemeApplicationService() ] } } extension AppDelegate { func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { ShortcutApplicationService() .application(application, performActionFor: shortcutItem, completionHandler: completionHandler) } func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { return DeepLinkApplicationService() .application(application, continue: userActivity, restorationHandler: restorationHandler) } }
Tell Me More!!
To accomplish this, we first create a protocol the AppDelegate
services will conform to:
public protocol ApplicationService { func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool func applicationWillEnterForeground(_ application: UIApplication) func applicationDidEnterBackground(_ application: UIApplication) func applicationDidBecomeActive(_ application: UIApplication) func applicationWillResignActive(_ application: UIApplication) func applicationProtectedDataWillBecomeUnavailable(_ application: UIApplication) func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) func applicationWillTerminate(_ application: UIApplication) func applicationDidReceiveMemoryWarning(_ application: UIApplication) func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) } // MARK: - Optionals public extension ApplicationService { func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { return true } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { return true } func applicationWillEnterForeground(_ application: UIApplication) {} func applicationDidEnterBackground(_ application: UIApplication) {} func applicationDidBecomeActive(_ application: UIApplication) {} func applicationWillResignActive(_ application: UIApplication) {} func applicationProtectedDataWillBecomeUnavailable(_ application: UIApplication) {} func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) {} func applicationWillTerminate(_ application: UIApplication) {} func applicationDidReceiveMemoryWarning(_ application: UIApplication) {} func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {} func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {} }
This simply mirrors the AppDelegate
events which we will plug into just a bit. Notice the protocol functions are optional because not all services will need to tap into all the events all the time.
Next, we create a super class for our AppDelegate
to bind the events to each of the services:
open class PluggableApplicationDelegate: UIResponder, UIApplicationDelegate { public var window: UIWindow? /// Lazy implementation of application services list public lazy var lazyServices: [ApplicationService] = services() /// List of application services for binding to `AppDelegate` events open func services() -> [ApplicationService] { return [ /* Populated from sub-class */ ] } } public extension PluggableApplicationDelegate { func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { return lazyServices.reduce(true) { $0 && $1.application(application, willFinishLaunchingWithOptions: launchOptions) } } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { return lazyServices.reduce(true) { $0 && $1.application(application, didFinishLaunchingWithOptions: launchOptions) } } } public extension PluggableApplicationDelegate { func applicationWillEnterForeground(_ application: UIApplication) { lazyServices.forEach { $0.applicationWillEnterForeground(application) } } func applicationDidEnterBackground(_ application: UIApplication) { lazyServices.forEach { $0.applicationDidEnterBackground(application) } } func applicationDidBecomeActive(_ application: UIApplication) { lazyServices.forEach { $0.applicationDidBecomeActive(application) } } func applicationWillResignActive(_ application: UIApplication) { lazyServices.forEach { $0.applicationWillResignActive(application) } } } public extension PluggableApplicationDelegate { func applicationProtectedDataWillBecomeUnavailable(_ application: UIApplication) { lazyServices.forEach { $0.applicationProtectedDataWillBecomeUnavailable(application) } } func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) { lazyServices.forEach { $0.applicationProtectedDataDidBecomeAvailable(application) } } } public extension PluggableApplicationDelegate { func applicationWillTerminate(_ application: UIApplication) { lazyServices.forEach { $0.applicationWillTerminate(application) } } func applicationDidReceiveMemoryWarning(_ application: UIApplication) { lazyServices.forEach { $0.applicationDidReceiveMemoryWarning(application) } } } public extension PluggableApplicationDelegate { func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { lazyServices.forEach { $0.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { lazyServices.forEach { $0.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } }
What’s happening is the services()
function is exposed for the AppDelegate
to override in order to provide the services that want to plug and play. The PluggableApplicationDelegate
will create a lazy property to prevent multiple instantiation of the services, then will finally call those functions when each of the app event fires.
The AppDelegate
inherits from the PluggableApplicationDelegate
and provides the service instances it desires:
@UIApplicationMain class AppDelegate: PluggableApplicationDelegate { override func services() -> [ApplicationService] { return [ LoggerApplicationService(), NotificationApplicationService() ] } }
And one of the application services would look something like this:
final class LoggerApplicationService: ApplicationService, HasDependencies { private lazy var log: LogWorkerType = dependencies.resolveWorker() func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { log.config(for: application) return true } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool { log.info("App did finish launching.") return true } func applicationDidReceiveMemoryWarning(_ application: UIApplication) { log.warn("App did receive memory warning.") } func applicationWillTerminate(_ application: UIApplication) { log.warn("App will terminate.") } }
Now the AppDelegate
is scalable and maintainable! Instead mixing responsibilities in the same object, your pluggable service binds to the app lifecycle and manages its own processes.
BONUS: Pluggable UIViewController!
The UIViewController
can reap the benefits by leveraging the same technique too. We will create a ControllerService
protocol for those that want to plug and play into the view controller life cycle:
public protocol ControllerService { func viewDidLoad(_ controller: UIViewController) func viewWillAppear(_ controller: UIViewController) func viewDidAppear(_ controller: UIViewController) func viewWillDisappear(_ controller: UIViewController) func viewDidDisappear(_ controller: UIViewController) func viewWillLayoutSubviews(_ controller: UIViewController) func viewDidLayoutSubviews(_ controller: UIViewController) } public extension ControllerService { func viewDidLoad(_ controller: UIViewController) {} func viewWillAppear(_ controller: UIViewController) {} func viewDidAppear(_ controller: UIViewController) {} func viewWillDisappear(_ controller: UIViewController) {} func viewDidDisappear(_ controller: UIViewController) {} func viewWillLayoutSubviews(_ controller: UIViewController) {} func viewDidLayoutSubviews(_ controller: UIViewController) {} }
Then we create a super UIViewController
class that will bind the services to its events:
open class PluggableController: UIViewController { /// Lazy implementation of controller services list public lazy var lazyServices: [ControllerService] = services() /// List of controller services for binding to `UIViewController` events open func services() -> [ControllerService] { return [ /* Populated from sub-class */ ] } open override func viewDidLoad() { super.viewDidLoad() lazyServices.forEach { $0.viewDidLoad(self) } } open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) lazyServices.forEach { $0.viewWillAppear(self) } } open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) lazyServices.forEach { $0.viewDidAppear(self) } } open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) lazyServices.forEach { $0.viewWillDisappear(self) } } open override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) lazyServices.forEach { $0.viewDidDisappear(self) } } open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() lazyServices.forEach { $0.viewWillLayoutSubviews(self) } } open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() lazyServices.forEach { $0.viewDidLayoutSubviews(self) } } }
And finally, your view controller will end up like this:
class ViewController: PluggableController { override func services() -> [ControllerService] { return [ ChatControllerService(), OrderControllerService() ] } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } }
An example of a controller service would be:
class ChatControllerService: ControllerService, HasDependencies { private lazy var chatWorker: ChatWorkerType = dependencies.resolveWorker() func viewDidLoad(_ controller: UIViewController) { chatWorker.config() } } extension ChatControllerService { func viewWillAppear(_ controller: UIViewController) { chatWorker.subscribe() } func viewWillDisappear(_ controller: UIViewController) { chatWorker.unsubscribe() } }
The controller services will have their functions triggered when the UIViewController
events fire.
BONUS 2: Pluggable Application for Android!
This technique is cross-platform and can actually work for Android as well… your Android team will thank you! This is what the ApplicationService
interface and PluggableApplication
base class look like:
interface ApplicationService { fun onCreate() {} fun onTerminate() {} fun onConfigurationChanged(newConfig: Configuration?) {} fun onActivityStarted(activity: Activity?) {} fun onActivityStopped(activity: Activity?) {} fun onActivityPaused(activity: Activity?) {} fun onActivityResumed(activity: Activity?) {} fun onActivityDestroyed(activity: Activity?) {} fun onActivitySaveInstanceState(activity: Activity?, bundle: Bundle?) {} fun onActivityCreated(activity: Activity?, bundle: Bundle?) {} } open class PluggableApplication: Application.ActivityLifecycleCallbacks, Application() { open lateinit var services: ArrayList<ApplicationService> override fun onCreate() { super.onCreate() registerActivityLifecycleCallbacks(this) services.forEach { it.onCreate() } } override fun onTerminate() { super.onTerminate() services.forEach { it.onTerminate() } } override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) services.forEach { it.onConfigurationChanged(newConfig) } } override fun onActivityPaused(p0: Activity?) { services.forEach { it.onActivityPaused(p0) } } override fun onActivityResumed(p0: Activity?) { services.forEach { it.onActivityResumed(p0) } } override fun onActivityStarted(p0: Activity?) { services.forEach { it.onActivityStarted(p0) } } override fun onActivityDestroyed(p0: Activity?) { services.forEach { it.onActivityDestroyed(p0) } } override fun onActivitySaveInstanceState(p0: Activity?, p1: Bundle?) { services.forEach { it.onActivitySaveInstanceState(p0, p1) } } override fun onActivityStopped(p0: Activity?) { services.forEach { it.onActivityStopped(p0) } } override fun onActivityCreated(p0: Activity?, p1: Bundle?) { services.forEach { it.onActivityCreated(p0, p1) } } }
And the FragmentService
and PluggableFragment
base class:
interface FragmentService { fun onAttach(context: Context?) fun onCreate(savedInstanceState: Bundle?) {} fun onActivityCreated(savedInstanceState: Bundle?) {} fun onStart() {} fun onViewCreated(view: View, savedInstanceState: Bundle?) {} fun onResume() {} fun onSaveInstanceState(outState: Bundle) {} fun onPause() {} fun onStop() {} fun onDestroyView() {} fun onDestroy() {} fun onDetach() {} } open class PluggableFragment: Fragment() { open lateinit var services: ArrayList<FragmentService> override fun onAttach(context: Context?) { super.onAttach(context) services.forEach { it.onAttach(context) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) services.forEach { it.onCreate(savedInstanceState) } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) services.forEach { it.onActivityCreated(savedInstanceState) } } override fun onStart() { super.onStart() services.forEach { it.onStart() } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) services.forEach { it.onViewCreated(view, savedInstanceState) } } override fun onResume() { super.onResume() services.forEach { it.onResume() } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) services.forEach { it.onSaveInstanceState(outState) } } override fun onPause() { super.onPause() services.forEach { it.onPause() } } override fun onStop() { super.onStop() services.forEach { it.onStop() } } override fun onDestroyView() { super.onDestroyView() services.forEach { it.onDestroyView() } } override fun onDestroy() { super.onDestroy() services.forEach { it.onDestroy() } } override fun onDetach() { super.onDetach() services.forEach { it.onDetach() } } }
You finally end up with a clean Application
like this:
class MyApplication: PluggableApplication() { override var services: ArrayList<ApplicationService> = { arrayListOf( ErrorApplicationService(), LoggerApplicationService(), AnalyticsApplicationService(), BootApplicationService(with: window), ShortcutApplicationService(), NotificationApplicationService(), ThemeApplicationService() ) }() }
Conclusion
The pluggable service technique will greatly reduce code and responsibilities for your application and controller life cycles. See a sample application using this technique to try it out.
Leave a Reply