現在制作中のアプリで、UITabBarControllerを使った場合の画面ごとの情報共有(更新)が必要だったために導入しました。
僕の場合は5つのタブの内、3つのタブでEureka というライブラリを使用して、TableView(form)を作っています。
発端
TabBarControllerではTabBarのボタンを押して画面を切り替えた時にその都度画面が再描画されるわけではないので、例えば「設定」タブで設定情報を変更して他のタブを表示しようとすると当然設定の変更が反映されていません。
viewWillAppear?
画面を切り替えるたびに viewWillAppear は呼び出されるため、viewWillAppear内で tableView.reloadData
を書けば画面切り替えのたびに更新されるんじゃね?という甘い考えは自分の環境では実現しませんでした。
負荷的には「viewWillAppear」で毎回再描画できたらする」 と 「Notificationでのイベント通知時だけの更新」どちらが適切なんだろう?
そこで色々調べた結果オブザーバパターン
Notification 及び Notification を使えば望み通りの動作が実現可能だと思い実装しました。
様々なSwiftバージョンの記事が見つかり混乱しながらも、ベストな書き方ではないと思いますが動作確認できましたので備忘録として書き留めておきます。
まず、通知を送りたい側のVCに通知をPostする処理を書く
インスタンス
let notificationCenter = NotificationCenter.default
通知を送りたい箇所でPost
// Notification通知を送る(通知を送りたい箇所に書く。例えば何らかのボタンを押した際の処理の中等)
notificationCenter.post(name: .myNotificationName, object: nil)
}
※ name: .myNotificationName
についてはこの下を参照
Notification.Name の拡張
上記で書いた name: .myNotificationName
ですが、"文字列"を通知送信側と受信側で書くのは危ういので Extentionを用いて Notification.Name の拡張をします。これにより notificationCenter.post(name: .ほにゃらら, object: nil)
と記載できます。
(どこでも良いと思いますが、PostViewController.swiftの一番下に追加しました。
extension Notification.Name {
static let myNotificationName = Notification.Name("myNotificationName")
}
次は通知を受け取りたい側
例えば通知によって何らかの表示を変えておきたいなど。
インスタンス
let notificationCenter = NotificationCenter.default
addObserverの登録
ここでも Extentionでの拡張により .myNotificationName
が使えます。
override func viewDidLoad() {
super.viewDidLoad()
/// NotificationCenterに登録する
notificationCenter.addObserver(self, selector: #selector(catchNotification(notification:)), name: .myNotificationName, object: nil)
}
catchNotificationを実装
func catchNotification(notification: Notification) -> Void {
print("Catch notification")
// 通知を受け取ったことにより行いたい処理を書く
}
}
removeObserverについて
登録した通知をそのままにしておくとメモリリークを起こしてしまう危険性があるため以下のようにremoveObserver()を書く必要があるとされていましたが……
deinit {
// Notification通知の削除
notificationCenter.removeObserver(self)
print("今、notificationCenter.removeObserverが発動")
}
調べていると、iOS 9から明示的にremoveObserver()を呼ぶ必要は無くなったそうです。
iOS 11が間も無く、といった現在にiOS8をサポートする必要はなさそうですので、概ね記述なしで良さそうです。
Eureka の場合
もっとスマートにやる方法がありそうですが、実現できず以下の方法で更新しています。
送る側
// 前略
<<< DateInlineRow("hireDate") {
$0.title = NSLocalizedString("Hire date", comment: "")
// UserDefaultsのデフォルト値を設定
userDefaults.register(defaults: ["hireDate": Date()])
$0.value = userDefaults.object(forKey: "hireDate") as? Date
$0.displayValueFor = {
return self.dateFormatter.string(from: $0 ?? Date())
}
$0.maximumDate = Date()
}.onChange { row in
print("Hire dateを \(row.value ?? Date()) に変更")
userDefaults.set(row.value, forKey: "hireDate")
// Notification通知を送る
self.notificationCenter.post(name: .changedSettingNotification, object: nil)
}
// 後略
受け取る側
// 前略
func catchNotification(notification: Notification) {
print("Catch notification")
// 更新前に情報を再度取得する
getUQDetails()
// 各行を更新するためにCellUpdateを走らせる
self.form.rowBy(tag: "lengthOfService")?.updateCell()
self.form.rowBy(tag: "precedingFiscalYear")?.updateCell()
self.form.rowBy(tag: "thisFiscalYear")?.updateCell()
self.form.rowBy(tag: "nextGrantDate")?.updateCell()
self.form.rowBy(tag: "nextIncreasing")?.updateCell()
tableView.reloadData()
}
// 中略
<<< LabelRow("lengthOfService") {
$0.title = NSLocalizedString("Length of service", comment: "")
$0.value = String(format: NSLocalizedString("YearsAndMonths", comment: ""), lengthOfService.year ?? 0, lengthOfService.month ?? 0)
}.cellUpdate { cell, row in
row.value = String(format: NSLocalizedString("YearsAndMonths", comment: ""), self.lengthOfService.year ?? 0, self.lengthOfService.month ?? 0)
}
+++ Section(NSLocalizedString("Breakdown of annual leave", comment: ""))
<<< LabelRow("precedingFiscalYear") {
$0.title = NSLocalizedString("Preceding fiscal year", comment: "")
$0.value = "\(Int(previousGrantedDays)) \(NSLocalizedString("days", comment: ""))"
}.cellUpdate { cell, row in
row.value = "\(Int(self.previousGrantedDays)) \(NSLocalizedString("days", comment: ""))"
}
<<< LabelRow("thisFiscalYear") {
$0.title = NSLocalizedString("This fiscal year", comment: "")
$0.value = "\(Int(currentGrantedDays)) \(NSLocalizedString("days", comment: ""))"
}.cellUpdate { cell, row in
row.value = "\(Int(self.currentGrantedDays)) \(NSLocalizedString("days", comment: ""))"
}
+++ Section(NSLocalizedString("Next fiscal year", comment: ""))
<<< LabelRow("nextGrantDate") {
$0.title = NSLocalizedString("Next grant date", comment: "")
$0.value = dateFormatter.string(from: grantDate ?? Date())
}.cellUpdate { cell, row in
row.value = self.dateFormatter.string(from: self.grantDate ?? Date())
}
<<< LabelRow("nextIncreasing") {
$0.title = NSLocalizedString("Next increasing", comment: "")
$0.value = "\(Int(nextGrantedDays)) \(NSLocalizedString("days", comment: ""))"
}.cellUpdate { cell, row in
row.value = "\(Int(self.nextGrantedDays)) \(NSLocalizedString("days", comment: ""))"
}
// 後略
問題点
- formごと全て更新する方法がわからず、結果全ての行のupdateCell()を呼んでいる。
- reloadDataだけではダメだった。せめてセクションごとに更新できれば...
- cellUpdate内のrow.value代入が同じことをそのまま書いているという二度手間っぷり。。
参考、及び勉強させていただきました
Swift 3 以降の NotificationCenter の正しい使い方 - Qiita
NSNotificationCenterのremoveObserverについて: スタック・オーバーフロー