お気に入り状態などを複数の画面で同期させる方法
アイテムをテーブルビューで一覧表示し、タップすると詳細画面に飛ぶ。 詳細画面でアイテムを「お気に入り」して、前の画面に戻ると、一覧ではお気に入りになってない。 みたいなパターンをよく作ると思います。
また、NSNotificationを使って「お気に入り」イベントを通知し、ビューの状態を変更しても、元のモデルが変更されていないため、テーブルビューをスクロールして再描画した時に、お気に入り状態が戻ってしまったりします。
このような場合には、「モデルを修正→モデルに基づいてビューを再描画する」という手順を守る必要があります。
下記ViewControllerでは、viewDidLoadでモデルの変更通知を受け取ります。 ModelChangeableを実装することで、モデルの変更を受け取り、modelsを変更すること、テーブルビューを再描画します。 こうすることで、データとビューに差が生まれることを防げきます。
class ViewController: UIViewController {
@IBOutlet fileprivate weak var tableView: UITableView!
var models = [Model]() {
didSet {
tableView?.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
addDidChangeModelObserver()
}
deinit {
removeDidChangeModelObserver()
}
}
extension ViewController: ModelChangeable {
func updateModel(_ model: Model) {
models = models.map({ $0.id == model.id ? model : $0 })
}
func deleteModel(_ model: Model) {
models = models.filter({ $0.id != model.id })
}
}
サンプルのモデルではidとお気に入り状態を保持します。
struct Model {
let id: Int
var isFavorite: Bool
}
変更されたモデル受け取るクラスが実装すべきプロトコルを宣言します。 今回のサンプルでは、モデルのUpdateとDelete操作に対応するとします。
protocol ModelChangeable {
func updateModel(_ model: Model)
func deleteModel(_ model: Model)
}
通知を送信したり、受信した通知のペイロードから値を取得するUtilityです。
enum NotificationEventType: Int {
case update
case delete
}
private enum NotificationInfoKey: String {
case model
case eventType
}
extension NSNotification.Name {
static let didChangeModel = Notification.Name(rawValue: "changed_model")
}
struct ModelNotification {
static func update(model: Model) {
post(model: model, eventType: .update)
}
static func delete(model: Model) {
post(model: model, eventType: .delete)
}
private static func post(model: Model, eventType: NotificationEventType) {
let userInfo: [String: Any] = [
NotificationInfoKey.model.rawValue: model,
NotificationInfoKey.eventType.rawValue: eventType
]
DispatchQueue.main.async {
NotificationCenter.default.post(name: .didChangeModel, object: nil, userInfo: userInfo)
}
}
static func getModel(from notification: Notification) -> Model? {
guard
let userInfo = notification.userInfo,
let model = userInfo[NotificationInfoKey.model.rawValue] as? Model else {
return nil
}
return model
}
static func getEventType(from notification: Notification) -> NotificationEventType? {
guard
let userInfo = notification.userInfo,
let eventType = userInfo[NotificationInfoKey.eventType.rawValue] as? NotificationEventType else {
return nil
}
return eventType
}
}
モデル変更を受け取りたいクラスが実装すべき関数を定義したプロトコルです。 例では、ViewControllerについて実装しています。
protocol ModelObserving {
func addDidChangeModelObserver(notificationCenter: NotificationCenter)
func removeDidChangeModelObserver(notificationCenter: NotificationCenter)
func didChangeModel(_ notification: Notification)
}
// MARK: - UIViewController
extension UIViewController: ModelObserving {
func addDidChangeModelObserver(notificationCenter: NotificationCenter = NotificationCenter.default) {
notificationCenter.addObserver(self, selector: #selector(didChangeModel(_:)), name: .didChangeModel, object: nil)
}
func removeDidChangeModelObserver(notificationCenter: NotificationCenter = NotificationCenter.default) {
notificationCenter.removeObserver(self, name: .didChangeModel, object: nil)
}
func didChangeModel(_ notification: Notification) {
guard
let modelChangeableThing = self as? ModelChangeable,
let model = ModelNotification.getModel(from: notification),
let eventType = ModelNotification.getEventType(from: notification) else {
return
}
switch eventType {
case .update:
modelChangeableThing.updateModel(model)
case .delete:
modelChangeableThing.deleteModel(model)
}
}
}
詳細画面において、モデルの状態に変更があった時に、通知を発行します。 そうすると、ModelChangeableを実装したViewControllerのupdateModelやdeleteModelが呼び出されます。
class DetailViewController: UIViewController {
@IBAction private func onFavoriteButtonClick(_ sender: UIButton) {
ModelNotification.update(model: model)
}
@IBAction private func onDeleteButtonClick(_ sender: UIButton) {
ModelNotification.delete(model: model)
}
}
Synchronize-Model@githubに動作するプロジェクトがあります。