[iOS] AppDelegateを綺麗に保つ4つのテクニック

`AppDelegate`はアプリ全体のライフタイムイベントを管理するためのクラスですが、その性質上、様々な処理が書かれやすいです。

しかし、あらゆる処理が書かれ肥大化していくと、見通しが悪くなってメンテナンスがしづらくなったり、チームで開発してる場合はコンフリクトが起こるなど開発速度に支障をきたすようになってしまう場合があります。

そこで、この記事では、そんな膨れがちな`AppDelegate`を綺麗な状態に戻すための方法をいくつか紹介します。


## 1. AppDelegateの責務外の処理は他クラスに移す

`AppDelegate`の主な責務はライフタイムイベントの管理です。具体的には「起動」「停止」「バックグラウンド状態の切り替わり」などなど[UIApplicationDelegateで定義されているような処理](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/index.html)です。

にもかかわらず、例えば全Controllerから触れる値を定義したいなどの理由で、責務と全く関係ないような処理を`AppDelegate`に書いてしまうと、どんどん肥大化していって役割が不明確になっていってしまいます。

そのような場合、各処理を役割にあった別のクラスに切り出してみると、見通しがよくなります。

### 改善例

#### before( ログイン状態のフラグをAppDelegateが管理 )


例えば、以下はログイン状態のフラグを`AppDelegate`に書いてしまっている例です。

```swift:Appdelegate.swifft
class AppDelegate: UIResponder, UIApplicationDelegate {
    ...
    // アプリのライフタイムサイクルと無関係であるログイン処理が
    // AppDelegateに書かれてしまっている
    var isLogin:Bool = false
    ...
}
````

```swift:HogeViewController
class HogeViewController: UIViewController {
    override func viewDidLoad() {
        ...
        // こうすれば全Controllerからログイン状態を取得できるが....
        let appDelegate:AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
        let isLogin = appDelegate.isLogin
    }
}
````

#### after( ログイン関係の処理はAuthServiceへ )

例えば以下のように`AuthService`といったようにログイン状態を管理することを責務としたクラスを作って、そこに処理を切り出すと`AppDelegate`はスッキリし、さらに`Controller`の処理も読みやすくなります。

```swift:AppDelegate.swifft
class AppDelegate: UIResponder, UIApplicationDelegate {
    // AppDelegateからはログイン関係の処理は消して専用クラスに移動させる
}
````

```swift:AuthService.swifft
// ログイン状態を管理するための専用のクラスを作る
class AuthService: UIResponder, UIApplicationDelegate {
    // AppDelegateにあったログイン関係の処理はこちらに全て移動
    var isLogin:Bool = false;
    class var sharedManager : AuthService { ... }
    func login(){ ... }
    func logout(){ ... }
}
````

```swift:HogeViewController
// コントローラーも、こちらのほうがシンプルになる
class HogeViewController: UIViewController {
    override func viewDidLoad() {
        ...
        let isLogin  = AuthService.sharedService.isLogin
        ...
    }
}
````

## 2. 細かな初期化処理をAppDelegateから各クラスに移す

アプリ起動後に`AppDelegate`の`didFinishLaunchingWithOptions`が呼ばれます。そのため、ここに全体的な機能の初期化処理を行うことが多いです。

しかし、ここで複雑な初期化処理があると`AppDelegate`の見通しが悪くなってしまうケースがあります。初期化自体、自分のクラスで行えるのであれば`AppDelegate`ではなく、各クラスの初期化時に行ったほうが良いです。

### 改善例

#### before( 細かな設定がAppDelegateで行われている )

```swift:AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        ...
        // リモートロガーの設定
        MyRemoteLogger.setBufferingInterval(5)
        MyRemoteLogger.setRemoteHost("http://example.com/logger")
        MyRemoteLogger.setAppName("MyApp")
        MyRemoteLogger.setTimeZone(xxx)
        ...
    }
}
````

#### after( 細かな初期化処理は、各クラス側へ )

```swift:AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // AppDelegateの中では以下を呼ぶだけ
        MyRemoteLogger.configure()
    }
}
````


```MyRemoteLogger.swift
class MyRemoteLogger:NSObject {
    class func configure(){
        // 複雑な初期化は各クラスで行う
        MyRemoteLogger.setBufferingInterval(5)
        MyRemoteLogger.setRemoteHost("http://example.com/logger")
        MyRemoteLogger.setAppName("MyApp")
        MyRemoteLogger.setTimeZone(xxx)
    }
}
````

また、初期化のタイミングが遅延（必要になったときに呼ぶ）できる設計に変更できる場合は、遅延させるようにすると`AppDelegate`から完全に初期化処理を消すこともできます。

#### 初期化してるクラスが外部ライブラリの場合は？

外部ライブラリを使ってる場合は、上のように自分でメソッドを生やしたりすることはできません。
そのような場合は、そのライブラリを利用するためのラッパークラスの作成を検討してみても良いかもしれません。

例えば`GoogleAnalytics`であれば直接`GAI`インスタンスを触るのではなく`AppNameAnalytics`のようなクラスを１つ作って、そこでアプリ独自の設定などを書く等です。

といっても、そのラッパークラスの中身が、初期化数行だけ等だと、逆に複雑になるだけの可能性もあるので規模や状況に応じて、検討してみてください。


## 3. NSNotificationCenterの利用を検討する

`AppDelegate`の主要な`delegate`メソッドは以下のように`NSNotificationCenter`でも監視することができます。

```swift:NSNotificationCenterでバックグラウンドへの移動を監視
 NSNotificationCenter.defaultCenter().addObserver( self,
    selector: "methodName",
    name: UIApplicationDidEnterBackgroundNotification,
    object: nil
)
````

### 改善例

例えば、アプリが何らかのサービスのHTTPなAPIを利用していて結果を端末に`cache`しているような場面があったとします。そして`アプリがバックグラウンドにいった際にcache`するような仕様だったとします。

そんなケースだと以下のように`AppDelegate`から、該当クラスに処理を移すことができます。

#### before( AppDelegateの中にcacheクリア処理がある )

```swift:AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {
    ...
    func applicationDidEnterBackground(application: UIApplication) {
        // AppDelegateの中でcacheのクリア処理を実行している
        APIClient.sharedClient.clearCache()
    }
    ...
}
````

#### after( cacheクリアもAPIClientクラスで完結出来る )

```swift:AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {
    func applicationDidEnterBackground(application: UIApplication) {
        // AppDelegateから該当の処理は消える
    }
}
````

```swift:APIClient.swift
class APIClient: NSObject {
    ...
    override init() {
        super.init()
        // 各クラスがNSNotificationCenterでバックグラウンド入りを監視
        NSNotificationCenter.defaultCenter().addObserver( self,
            selector: "clearCache",
                name: UIApplicationDidEnterBackgroundNotification,
              object: nil
        )
    }
    ...
}
````

## 4. AppDelegateをサブクラスに分割する

どうしても`AppDelegate`で実行しなければいけない処理が多様にある場合もあります。例えば広告などの各種SDKが指定されていて、多くの会社のSDKを導入している場合などですね。

そのような処理が多く、見通しが悪くなっている場合は、種類ごとにAppDelegateをまとめてサブクラスにすると全体の見通しをよくすることができるかもしれません。

### 改善例

#### before( 様々な種類の処理が混ざっている )

```swift
class AppDelegate: UIResponder, UIApplicationDelegate {
    ...
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // 広告会社AのSDK処理
        AdsADK_A.sharedSDK.onApplicationDidFinishLaunching()

        // 広告会社BのSDK処理
        AdsADK_B.registerConvertionID("xxx", mediaKey:"yyy")

        // 広告会社CのSDK処理
        AdsADK_C.register(appKey:"aaa")
        
        // 広告以外の処理
    }
    ...
    // 他のメソッドでも同様にSDKの決まりの処理が多くて、他の処理が見づらくなっている
}
````

#### after( 広告SDK周りの処理のみサブクラスへ委譲 )

```swift
class AppDelegate: UIResponder, UIApplicationDelegate {
    ...
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

        // 広告の処理は別クラスAppDelegateForAdsADKを作り、そちらへ委譲
        AppDelegateForAdsADK.sharedDelegate.application(application, 
            didFinishLaunchingWithOptions: launchOption
        )
        
        // 広告以外の処理
    }
    // 他のdelegateメソッドでも同様に変更
}
````

## まとめ

自分が`AppDelegaet`を整理する中で、意識したところや試した方針をまとめてみました！並べてみると、どれも「クラスの責務を適切に分解する」という基本的な原則に従うための方針になっていますね。

もし`AppDelegate`が肥大化していて整理したい！という状況の方がいれば参考にしてみてください。