Skip to main content
すっさんぽ
  1. Posts/

CompositeパターンでAppDelegateの肥大化を防ぐ

iOSアプリ開発では、AppDelegateの肥大化がしばしば課題に上がります。 例えば、 didFinishLaunchingWithOptionsメソッドが代表的ですが、AppDelegateでは依存ライブラリの初期化であったり、アプリケーションの各種ロジックを初期化したりと、様々な処理がなされます。

それだけではありません。プッシュ通知を受信したときの didReceiveRemoteNotificationもそうですし、アプリケーションのライフサイクル applicationDidBecomeActive なども、AppDelegateに通知されます。

とにかくAppDelegateは、アプリケーションの初期化、OS・システムからの通知の受け口として、ロジックが増えがちです。

Compositeパターンを使用して肥大化を防ぐ #

あるとき、AppDelegateの肥大化を防ぐ方法を探していると、とあるブログに辿り着きました。

Vadim BulavinさんのブログポストRefactoring Massive App Delegateです。

こちらのブログで、Compositeパターンを使用した方法が紹介されています。実際にやってみたところ、設計が理解しやすく、書きやすいことがわかりました。

こちらのブログで紹介されていた方法を私もやってみたので、その時のコードをベースに方法を紹介します。

Compositeパターンについて #

予め、Compositeパターンがどんなものなのかを見ておきます。いくつかサイトを見てみます。

「容器と中身を同一視する」ことで、再帰的な構造の取り扱いを容易にするものです。

https://www.techscore.com/tech/DesignPattern/Composite

「木構造(Tree Structure)」を持つデータに「再帰的な処理」を行うことができる

https://www.pgls-kl.com/article/article_81.html

要するに、

  • 再帰的に処理を伝播させることを得意としているパターンである
  • 処理を伝播させる方も、伝播される方も、同じインタフェースを持っている

ということが特徴です。

実際にやってみた #

では、ここから先は、私が上のブログを読んで実際に試してみたコードを使って、解説していきます。

AppDelegate #

まず、AppDelegateを見てみます。 私が試したときのコードを、そのままコピペします。

import UIKit

enum AppDelegateFactory {
    static func makeDefault() -> AppDelegateType {
        CompositeAppDelegate(appDelegates: [RemoteNotificationAppDelegate(), UserNotificationAppDelegate()])
    }
}

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    let appDelegate = AppDelegateFactory.makeDefault()

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

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

    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        appDelegate.application?(application, didFailToRegisterForRemoteNotificationsWithError: error)
    }

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        appDelegate.application?(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
    }
}

AppDelegateでやっていることは、各Delegateメソッドを、インスタンス変数の appDelegate に伝播させているだけです。 appDelegateの正体は、AppDelageteFactorymakeDefault()メソッドを呼んで生成したCompositeAppDelegateのインスタンスです。

CompositeAppDelegate #

ここで、今回のパターンで大切な AppDelegateType が定義されています。 AppDelegateTypeは、UIResponder & UIApplicationDelegateのエイリアスです。これはまさにAppDelegateと同じです。 つまり、このAppDelegateTypeを実装したものは、AppDelegateと同じインタフェース=メソッドを持っているということになります。

CompositeAppDelegateは、AppDelegateのメソッド呼び出しを、AppDelegateTypeのインスタンスに再帰的に伝播させることを担っています。

import UIKit

typealias AppDelegateType = UIResponder & UIApplicationDelegate

class CompositeAppDelegate: AppDelegateType {
    private let appDelegates: [AppDelegateType]

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

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        appDelegates.forEach {
            _ = $0.application?(application, didFinishLaunchingWithOptions: launchOptions)
        }
        return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        appDelegates.forEach {
            _ = $0.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
        }
    }

    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        appDelegates.forEach {
            $0.application?(application, didFailToRegisterForRemoteNotificationsWithError: error)
        }
    }

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        appDelegates.forEach {
            $0.application?(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
        }
    }
}

AppDelegateTypeの実装 #

残りの作業は、単一の責務を持つAppDelegateTypeを実装していくだけです。 今回は、リモート通知(プッシュ通知の受信)と、ユーザー通知(ユーザーにプッシュ通知を表示する)の2つを実装しました。 AppDelegate本体で実装するのと同じように書けるので、とても理解しやすく、書きやすいです。

RemoteNotificationAppDelegate #

リモート通知の受信に必要な処理だけを実装できています。 このあたりが、AppDelegateに直接実装されていると、AppDelegateの見通しが悪くする要因になります。

class RemoteNotificationAppDelegate: AppDelegateType {
    let logger = Logger(subsystem: "PushNotificationPractice", category: "RemoteNotification")

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

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let tokenParts = deviceToken.map { data -> String in
            return String(format: "%02.2hhx", data)
        }
        let token = tokenParts.joined()
        logger.info("Device Token: \(token)")
    }

    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        logger.error("Failed to register for remote notifications with error: \(error.localizedDescription)")
    }

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        logger.info("Push Notification Received: \(userInfo)")

        let content = UNMutableNotificationContent()
        content.title = "Background Push Received"
        content.body = "received at \(Date())"
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3.0, repeats: false)
        let request = UNNotificationRequest(identifier: "test", content: content, trigger: trigger)
        UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)

        completionHandler(.newData)
    }
}

UserNotificationAppDelegate #

こちらは、ユーザー通知に必要な処理を担っています。 UNUserNotificationCenterDelegateも継承し、UserNotificationCenterからの通知を受けられるようにしています。

class UserNotificationAppDelegate: AppDelegateType, UNUserNotificationCenterDelegate {
    let logger = Logger(subsystem: "PushNotificationPractice", category: "UserNotification")

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        return true
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        logger.debug("\(#function) called.")
        completionHandler()
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        logger.debug("\(#function) called.")
        completionHandler([.badge, .banner, .list, .sound])
    }
}

まとめ #

Vadim BulavinさんのRefactoring Massive App Delegateを参考にして、Compositeパターンを使ったAppDelegateの肥大化を防ぐ方法を試してみました。構造が理解しやすく、実装もしやすいので、既存のアプリにも導入しやすいと感じました。 また、単なるクラス切り出しではなく、処理を切り出したクラスもAppDelegateと同様に実装することができるのが、Compositeパターンを使うメリットだなと感じます。

Vadim Bulavinさんは、「巨大なAppDelegateは、ソフトウェアのデザインパターンを適用することで、単一の責任を持ち個別にテストできるいくつかのクラスに分割可能だ」と書いています。iOSアプリの開発では、アプリのアーキテクチャに目が行きがちですが、デザインパターンを適切に適用することも大事だと改めて感じました。