7
5

More than 5 years have passed since last update.

アプリ全体のライフサイクルをViewControllerで観測する

Posted at

iOS その2 Advent Calendar 2016の19日目の記事です。

はじめに

ViewControllerにおいて、アプリがForeground/Backgroundになった際に何か処理を行いたいケースを考えてみます。
ex. APIへのポーリングリクエストをBackground時はポーズする、Foregroundになったらレジュームするなど

(因みに、AndroidのActivityやFragmentはアプリ単位のライフサイクル通知を受け取れますね。。)

実装

組み込みのNSNotificationをラップする実装方針で進めます。
また、Foreground/Backgroundの通知だけではなくその他任意の通知ケース1も考えてみます。
(例として、アプリのState保存時の通知/観測も実装)

観測者(Observer)

通知の観測者(受信者)の振る舞いを示すprotocolを用意。
ここでいう観測者は表題の通り、実体はViewControllerになります。

AppLifecycleObserver.swift
@objc protocol AppLifecycleObserver: class {

    @objc optional func observeApplicationWillEnterForeground()

    @objc optional func observeApplicationDidEnterBackground()

    @objc optional func observeShouldSaveApplicationState()
}

全ての通知を受信する必要は無いので、各振る舞いはoptionalで定義しておきます。

送信者(Observable)

通知の送信者(観測される側)の振る舞いを示すprotocolを用意。
ここでいう送信者はAppDelegateにあたります。

AppLifecycleObservable.swift
protocol AppLifecycleObservable {

    func addObserver<T: AppLifecycleObserver>(observer: T, selector: Selector, name: NSNotification.Name) where T: UIViewController

    func removeObserver<T: AppLifecycleObserver>(observer: T, name: NSNotification.Name) where T: UIViewController
}

プロダクションで使用する、送信者を作成します。

AppLifecycleSubject.swift
struct AppLifecycleSubject: AppLifecycleObservable {

    static let sharedInstance = AppLifecycleSubject()

    private let notificationCenter = NotificationCenter.default

    private init () {}

    func addObserver<T: AppLifecycleObserver>(observer: T, selector: Selector, name: NSNotification.Name) where T : UIViewController {
        notificationCenter.addObserver(observer, selector: selector, name: name, object: nil)
    }

    func removeObserver<T: AppLifecycleObserver>(observer: T, name: NSNotification.Name) where T : UIViewController {
        notificationCenter.removeObserver(observer, name: name, object: nil)
    }

    func notifyShouldSaveApplicationState() {
        notificationCenter.post(name: .ShouldSaveApplicationState, object: nil)
    }
}

extension Notification.Name {

    static let ShouldSaveApplicationState: NSNotification.Name = NSNotification.Name(rawValue: "ShouldSaveApplicationState")
}

コンテナを介したDI機構が存在する前提ですが、ViewControllerのテスト時はMock用のObservableを使用することもできます。

MockAppLifecycleSubject.swift
struct MockAppLifecycleSubject: AppLifecycleObservable {

    private(set) var addObserverWasCalled = false
    private(set) var removeObserverWasCalled = false

    func addObserver<T: AppLifecycleObserver>(observer: T, selector: Selector, name: NSNotification.Name) where T : UIViewController {
        addObserverWasCalled = true
    }

    func removeObserver<T: AppLifecycleObserver>(observer: T, name: NSNotification.Name) where T : UIViewController {
        removeObserverWasCalled = true
    }
    // ...
}

AppDelegateから通知

AppLifecycleSubjectにおいてState保存時の通知処理は実装しましたが、
Foreground/Backgroundの通知に関しては、オリジナルの通知を登録するのではなくシステムが用意している通知を使います。(これらの名前の通知は自動的にAppDelegateからポストされます)

なので、State保存時のみ明示的に通知をポスト。

AppDelegte.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

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

    func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
        defer { AppLifecycleSubject.sharedInstance.notifyShouldSaveApplicationState() }
        return true
    }
}

ViewControllerで観測

通知を受信するObserver(ViewController)の実装を行います。

ViewController.swift
final class ViewController: UIViewController {

    private let appLifecycleSubject: AppLifecycleObservable = AppLifecycleSubject.sharedInstance

    override func viewDidLoad() {
        super.viewDidLoad()
        subscribeAppLifecycleNotification()
    }

    deinit {
        unsubscribeAppLifecycleNotification()
    }

    private func subscribeAppLifecycleNotification() {
        appLifecycleSubject.addObserver(observer: self, selector: .observeApplicationWillEnterForeground, name: .UIApplicationWillEnterForeground)
        appLifecycleSubject.addObserver(observer: self, selector: .observeApplicationDidEnterBackground, name: .UIApplicationDidEnterBackground)
        appLifecycleSubject.addObserver(observer: self, selector: .observeShouldSaveApplicationState, name: .ShouldSaveApplicationState)
    }

    private func unsubscribeAppLifecycleNotification() {
        appLifecycleSubject.removeObserver(observer: self, name: .UIApplicationWillEnterForeground)
        appLifecycleSubject.removeObserver(observer: self, name: .UIApplicationDidEnterBackground)
        appLifecycleSubject.removeObserver(observer: self, name: .ShouldSaveApplicationState)
    }
}

// MARK: - AppLifecycleObserver

extension ViewController: AppLifecycleObserver {
    func observeApplicationWillEnterForeground() {
        print("#function = \(#function)")
    }

    func observeApplicationDidEnterBackground() {
        print("#function = \(#function)")
    }

    func observeShouldSaveApplicationState() {
        print("#function = \(#function)")
    }
}

// MARK: - Selector

private extension Selector {
    static let observeApplicationWillEnterForeground = #selector(ViewController.observeApplicationWillEnterForeground)
    static let observeApplicationDidEnterBackground = #selector(ViewController.observeApplicationDidEnterBackground)
    static let observeShouldSaveApplicationState = #selector(ViewController.observeShouldSaveApplicationState)
}

ここまでで、アプリを実行しForeground/Background状態を繰り返すとログが出力されるのを確認できました!
以下、サンプルの実装です。
to4iki/AppLifecycleObserver

本記事の内容から脱線しますが、
Swift 2.2 で導入された#selectorをViewControllerごとに一元管理して可読性を上げよう
に記載の通り、セレクターを一元管理することで可読性があがりますね。

Conclusion

  • ObservableをI/Fとして定義することで内部実装は置き換え可能(今回は簡易的にNSNotificationを使用)。またテスタビリティが向上する
  • ViewControllerでの通知登録/解除がどうしても冗長になる(ここら辺うまいこと解決したいが、いい実装が思い浮かばなかったです)

以上、この記事が読んだ人の開発効率向上につながれば幸いです。

See also


  1. よい例が思い浮かばなかった 

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5