iOS その2 Advent Calendar 2016の19日目の記事です。
はじめに
ViewControllerにおいて、アプリがForeground/Backgroundになった際に何か処理を行いたいケースを考えてみます。
ex. APIへのポーリングリクエストをBackground時はポーズする、Foregroundになったらレジュームするなど
(因みに、AndroidのActivityやFragmentはアプリ単位のライフサイクル通知を受け取れますね。。)
実装
組み込みのNSNotificationをラップする実装方針で進めます。
また、Foreground/Backgroundの通知だけではなくその他任意の通知ケース1も考えてみます。
(例として、アプリのState保存時の通知/観測も実装)
観測者(Observer)
通知の観測者(受信者)の振る舞いを示すprotocolを用意。
ここでいう観測者は表題の通り、実体はViewControllerになります。
@objc protocol AppLifecycleObserver: class {
@objc optional func observeApplicationWillEnterForeground()
@objc optional func observeApplicationDidEnterBackground()
@objc optional func observeShouldSaveApplicationState()
}
全ての通知を受信する必要は無いので、各振る舞いはoptionalで定義しておきます。
送信者(Observable)
通知の送信者(観測される側)の振る舞いを示すprotocolを用意。
ここでいう送信者はAppDelegateにあたります。
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
}
プロダクションで使用する、送信者を作成します。
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を使用することもできます。
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からポストされます)
- NSNotification.Name#UIApplicationWillEnterForeground
- NSNotification.Name#UIApplicationDidEnterBackground
なので、State保存時のみ明示的に通知をポスト。
@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)の実装を行います。
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
- iPhoneアプリのライフサイクル
- [Android] アプリのbackground/foregroundを検知する
- マツコの知らない State Restoration の世界
- Swiftのプロトコル | オプショナルプロトコル要求(Optional Protocol Requirements)
-
よい例が思い浮かばなかった ↩