NotificationCenterを用いて、iOSアプリのライフサイクルイベントを検知する

#はじめに
iOSアプリがフォアグラウンド、バックグラウンドに入った等のライフサイクルイベントを、NotificationCenterを用いて検知する方法について書いていきます。このNotificationCenterの使用例として、アプリがバックグランドに入って30分以上経過した状態でフォアグラウンドに戻った時に、API通信を行う方法をRxSwift、ViewModelを使用して説明します。

#環境
- Swift：5.3
- Xcode：12.1

#iOSアプリのライフサイクル
下記がiOSアプリのライフサイクルの図です。
[Managing Your App's Life Cycle](https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle)より引用。
![iOS_Lifecycle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590610/b2b5e07b-6f95-66cc-cb57-ca2280c66564.png)

| 状態 | 内容 |
|:-:|:-:|
|Unattached   |アプリが未起動の状態。   |
|Background   |アプリがバックグラウンドで実行中。   |
|Foreground Inactive   |アプリがフォアグラウンドで実行中だが、イベントは受信していない。別の状態に切り替わる時に、一瞬この状態になる。   |
|Foreground Active   |アプリがフォアグラウンドで実行中。アプリを使用している時は基本的にこの状態。   |
|Suspended   |アプリがバックグラウンドで未実行。    |

#状態遷移時に呼ばれるメソッド
| メソッド | 呼ばれるタイミング |
|:-:|:-:|
|func application(_:willFinishLaunchingWithOptions)   |アプリ起動後   |
|func application(_:didFinishLaunchingWithOptions:)   |アプリが画面を表示する直前   |
|func sceneWillEnterForeground(UIScene)   |フォアグラウンドで実行を開始しようとする時   |
|func sceneDidBecomeActive(UIScene)   |アクティブになり、イベントを受け取れる状態になった時   |
|func sceneWillResignActive(UIScene)   |アクティブ状態から離れ、イベントの受信を止めようとしている時   |
|func sceneDidEnterBackground(UIScene)   |バックグラウンドに入った時   |
|func applicationWillTerminate(UIApplication)           |アプリが終了する時    |

アプリ全体で状態遷移のイベントを検知し、何らかのメソッドを実行したい場合はAppDelegate、SceneDelegateにある上記の各メソッド内に記述すれば問題ありません。ただ、場合によってはViewControllerで各状態遷移のイベントを検知したいことがあると思いますので、その方法について記述していきます。

#ViewControllerで状態遷移イベントを検知する
状態遷移イベントを検知するNotificationCenterが用意されているので、それを使用します。
他にもありますが、主に下記4つがあります。

| Notification |通知するタイミング |
|:-:|:-:|
|willEnterForegroundNotification   |フォアグラウンドで実行を開始しようとする時   |
|didBecomeActiveNotification   |アクティブになり、イベントを受け取れる状態になった時   |
|willResignActiveNotification   |アクティブ状態から離れ、イベントの受信を止めようとしている   |
|didEnterBackgroundNotification   |バックグラウンドに入った時   |

#NotificationCenterが通知するタイミング
##アプリ起動時
- willEnterForegroundNotification
- didBecomeActiveNotification

##アプリをバックグラウンドへ
- willResignActiveNotification
- didEnterBackgroundNotification

##コントロールセンターを表示して閉じる
- 表示
 - willResignActiveNotification
- 閉じる
 - didBecomeActiveNotification

##通知センターを表示して閉じる
- 表示
 - willResignActiveNotification
 - didBecomeActiveNotification（←なぜか呼ばれる）
 - willResignActiveNotification（←2度目が呼ばれる）
- 閉じる
 - didBecomeActiveNotification

#使用例
今回はアプリがバックグランドに入って30分以上経過した状態でフォアグラウンドに戻った時にAPI通信を行うようにします。RxSwiftを使用して、ViewModelとバインディングします。

```ViewController.swift
import UIKit
import RxSwift
import RxCocoa

final class ViewController: UIViewController {
    
    private let viewModel = ViewModel()
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bindOutput()
        bindIntput()
    }

    private func bindInput() {
        NotificationCenter.default.rx.notification(UIApplication.willResignActiveNotification)
            .subscribe(onNext: { [unowned self] _ in
                // アプリがアクティブではなくなる時の時間を取得
                self.viewModel.inputs.willResignActiveTime.onNext(Date())
        })
        .disposed(by: disposeBag)
        
        NotificationCenter.default.rx.notification(UIApplication.didBecomeActiveNotification)
            .subscribe(onNext: { [unowned self] _ in
                // アプリがアクティブになった時の時間を取得
                self.viewModel.inputs.didBecomeActiveTime.onNext(Date())
            })
            .disposed(by: disposeBag)
    }
    
    private func bindOutput() {
        viewModel.outputs.refreshData
            .subscribe(onNext: { [unowned self] in
                // API通信する処理を記載
            })
            .disposed(by: disposeBag)
    }
}
```

```Viewmodel.swift
import RxCocoa
import RxSwift

protocol ViewModelInput {
    var willResignActiveTime: PublishSubject<Date> { get }
    var didBecomeActiveTime: PublishSubject<Date> { get }
}

protocol ViewModelOutput {
    var refreshData: PublishSubject<Void> { get }
}

protocol ViewModelType {
    var inputs: ViewModelInput { get }
    var outputs: ViewModelOutput { get }
}

final class ViewModel: ViewModelType, ViewModelInput, ViewModelOutput {
    
    var inputs: ViewModelInput { return self }
    var outputs: ViewModelOutput { return self }
    
    private let disposeBag = DisposeBag()

    // Inputs
    var willResignActiveTime = PublishSubject<Date>()
    var didBecomeActiveTime = PublishSubject<Date>()

    // Outputs
    public var refreshData = PublishSubject<Void>()
    
   init() {
        setBind()
    }
    
    private func setBind() {
        let thirtyMinutesPassed = didBecomeActiveTime
            .withLatestFrom(willResignActiveTime) { [unowned self] didBecomeActiveTime, willResignActiveTime in
                self.checkThirtyMinutesPassed(didBecomeActiveTime, willResignActiveTime)
        }
        
        thirtyMinutesPassed
            .filter { $0 }
            .subscribe(onNext: { [unowned self] _ in
                // バックグラウンドに入って30分以上経過した場合更新する
                self.outputs.refreshData.onNext(())
            })
            .disposed(by: disposeBag)
    }
    
    // アプリがバックグラウンドに入ってから30分経過したか判定
    // バックグラウンドに入った時間とアプリがアクティブになった時間の差が30分（30*60秒=1800秒）以上の時はtrueを返す
    private func checkThirtyMinutesPassed(_ didBecomeActiveTime: Date, _ willResignActiveTime: Date) -> Bool {
        let convertedDidEnterBackgroundTime = Int(willResignActiveTime.timeIntervalSince1970)
        let convertedDidBecomeActiveTime = Int(didBecomeActiveTime.timeIntervalSince1970)
        return convertedDidBecomeActiveTime - convertedDidEnterBackgroundTime >= 1800
    }
}

```

今回はアプリがバックグランドに入った時、フォアグラウンドに戻った時の時刻をそれぞれNotificationCenterを使用して取得し、その差が30分以上の場合にAPI通信をするように実装しました。

アプリがフォアグラウンドになった状態検知に`didBecomeActiveNotification`を使用しました。アプリがフォアグラウンドになる時には`willEnterForegroundNotification`でも通知されますが、まだアプリが完全にアクティブな状態でない時に通知されるため、正常にメソッドが実行されない懸念があります（今回で言うとAPI通信する処理）。そのため、確実にアクティブな状態になった時に通知される`didBecomeActiveNotification`を使用するのが良いです。

アプリがバックグラウンドに入った状態検知には`willResignActiveNotification`を使用しました。`didEnterBackgroundNotification`でもバックグラウンドに入ったことを検知できますが、コントロールセンター/通知センターを使用する時に問題が発生します。
その理由は、コントロールセンター/通知センターを表示する際には`didEnterBackgroundNotification`は通知されませんが、閉じた際には`didBecomeActiveNotification`が通知されるからです。例えば下記のようなことが発生してしまいます。

```
12:00 バックグラウンドに入る（didEnterBackgroundNotification）
12:31 バックグランドから復帰（didBecomeActiveNotification）
12:32 通知センター/コントロールセンターを開く
-> didEnterBackgroundNotificationは呼ばれず、バックグラウンドに入った時刻は12:00のまま
12:33 通知センター/コントロールセンターを閉じる
-> didBecomeActiveNotificationが呼ばれ、復帰した時刻は12:33
-> 12:00と12:33で差が30分以上のため、通知センター/コントロールセンターを表示してすぐ閉じた場合でもメソッドが実行される
```

今回はAPI通信処理を実行するので、通知センター/コントロールセンターを表示してすぐ閉じた場合にもメソッドが実行されてしまうと、無駄な通信が発生することになってしまいます。このような事を避けるため、通知センター/コントロールセンターを表示した場合にも通知される`willResignActiveNotification`を使用しました。これで通知センター/コントロールセンターを表示してすぐ閉じた場合にメソッドが実行されるということはなくなります。

#参考文献
- [Managing Your App's Life Cycle](https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle)
- [UIApplication](https://developer.apple.com/documentation/uikit/uiapplication)
- [iOSアプリの状態遷移とライフサイクル
](https://qiita.com/ya8612tec/items/52a39054eadc4742c267)