お待たせしました 👋
先週のチュートリアルに挑戦していただけた方は、アプリを起動すると以下のような画面が表示されていると思います。
(私は先週のステップ7.4でテスト用のピンの位置をサンフランシスコにしましたが、もしみなさんが違う場所を指定していた場合ピンは別の位置に立っていることでしょう。私の現在位置もサンフランシスコなので地図もここのようになっていますが、皆さんの現在位置に応じて地図も別の場所を指していると思います。シミュレーターがどのように現在位置を把握しているのかまったくわかりませんが、とにかくそうなっていると思います。)
これだけでも立派なアプリですが、正直まだ大したことはしていません。実際、ピンの位置以外はハードコートされたものです。さっそく修正していきましょう。
このパートでは、すべてのOutletをRealmから取得した値で設定していきます。そのためにはいくつかのモデル定義の追加が必要になりますが、これは次回Realm Object Serverへ接続するためにも重要な作業です。
1. モデルの追加
現時点でハードコートされている値を表現するためのモデル定義を追加していきます。UIをみればわかるように、以下のものをモデルとして表現する必要があります。
- サンタクロース到着までの時間
- サンタクロースが今何をしているか
- サンタクロースがいる場所の天気
- 配り終えていないプレゼントの数
これらを順番に作っていきましょう。
サンタクロース到着までの時間
サンタクロースが到着する時刻がわかっていれば、サーバーに問い合わせるまでもなくデバイス上で計算することができます。到着時刻に関する情報は、来週サーバーに接続して取得します。これらの情報をSanta
クラスに追加しておきます。
class Santa: Object {
// currentLocation等の先週のコードはこの上
let route = List<Stop>()
// ignoredProperties はこの下
}
サンタクロースの経路というのは、いつおよびどこという2つの情報のリストとして表すことができます。これをStop
というクラスで表現します。ドキュメントの「1対多のリレーションシップ」を見ていただければわかると思いますが、モデルのリストはList
クラスで表現することができます(名前の通りですね)。モデルのリストを表現する際の書き方は、上記のコードの通りです。このままではStop
クラスが定義されていないためコンパイルエラーになってしまします。Stop
クラスを新たなファイルStop.swift
に定義しましょう。
class Stop: Object {
dynamic var location: Location?
dynamic var time: Date = Date(timeIntervalSinceReferenceDate: 0)
convenience init(location: Location, time: Date) {
self.init()
self.location = location
self.time = time
}
}
繰り返しになりますが、Stop
はサンタクロースがいつどこに到着するかを表しています。言うまでもなくサンタクロースのスケジュールはとてもきっちりしているので、遅れたり早く着くことはありません。彼は何百年も繰り返しプレゼントを配っているので、スケジュールの正確さに関しては信用できます。
先週のデータモデルの定義のステップで追加したLocation
クラスを再利用しています。また、到着日時を表すためにDate
型のプロパティも追加しています。このDate
型からもわかるように、Realmもモデル定義にはSwift 3 のFoundation classも利用可能です。
ここでもconvenience initializerを定義しています。このことについて先週言及するのを忘れていました。ドキュメントの「モデルクラスにカスタム定義のイニシャライザを追加する」にあるように、モデルクラスのカスタムイニシャライザはconvenience
キーワードが必須で、親クラスのイニシャライザをsuper
で呼び出すのではなく、self.init()
を使う必要があります。これはSwiftのintrospectionの制限によるものです。
到着時刻の計算アルゴリズムについては以降の回で考えることにします。素晴らしい解決方法を用意しているので心配しないでください。実はもう書き終わっているのでお約束できます。
サンタクロースが今何をしているか
地図の下に今サンタクロースが何をしているか表示する欄があります。おそらくどこかを飛び回っているか、プレゼントを配っているか、ミセス・クロースとお話をしているかでしょう。みなさんがどう感じたかはわかりませんが、私にはこれはSwiftの列挙型(enum)で表すのにぴったりだと思いました。残念なことに、現時点のRealmは列挙型を直接扱うことができません。ただし、列挙型自体ではなくそのrawValue
を扱うことはできます。
列挙型のActivity
のために新たなファイルActivity.swift
を作成してください。
enum Activity: Int {
case unknown = 0
case flying
case deliveringPresents
case tendingToReindeer
case eatingCookies
case callingMrsClaus
}
この列挙型はInt
で表すことができるように定義されています。そしてInt
であればRealmで扱うことが可能です。String
で表現することもできましたが、列挙型を使いたかったのでInt
にしています。ここでは値をはっきりさせるために1つめの要素に対して0
を明示しています。Swiftコンパイラは、値が未指定の要素には一つ前の値に1
を加えた値を使うようになっています。このようにしておくことで、他のプラットフォームとモデル定義を共通化する際にわかりやすくなります。
それぞれの値に対応するテキストをextension
としてActivity
のすぐあとにに定義しておきます。View Controllerや専用のクラス等で変換を行うこともできますが、シンプルにするために同じファイルに定義します。
extension Activity: CustomStringConvertible {
var description: String {
switch self {
case .unknown:
// "❔ なにをしているか不明です..."
return "❔ We're not sure what Santa's up to right now…"
case .callingMrsClaus:
// "📞 奥さんと電話中です!"
return "📞 Santa is talking to Mrs. Claus on the phone!"
case .deliveringPresents:
// "🎁 プレゼントを配っています!"
return "🎁 Santa is delivering presents right now!"
case .eatingCookies:
// "🍪 おやつをたべています。"
return "🍪 Santa is having a snack of milk and cookies."
case .flying:
// "🚀 つぎの家まで移動中です。"
return "🚀 Santa is flying to the next house."
case .tendingToReindeer:
// "𐂂 トナカイのお世話をしています。"
return "𐂂 Santa is taking care of his reindeer."
}
}
}
実際のアプリではこれらの文字列はローカライズする必要があるかと思いますが、私は英語しか話せません。😓 みなさんはそれぞれの言語に翻訳していただいてもかまいませんし、内容自体を書き換えてしまってもかまいません。
Activity
の定義が完成したので、これもサンタクロースに追加しましょう。
class Santa: Object {
// 現在位置や経路の情報はこの上
private dynamic var _activity: Int = 0
var activity: Activity {
get {
return Activity(rawValue: _activity)!
}
set {
_activity = newValue.rawValue
}
}
// ignoredPropertiesはこの下
}
activity
プロパティの型は列挙型のActivity
として扱えるようにするのが自然です。この情報をRealmで扱えるようにするため、内部ではInt
として扱います。プライベート変数としてInt
型の_activity
プロパティを定義し、外部に見せる際はActivity
に変換します。これで、Realmのモデル定義が列挙型のプロパティを持っているように見せることができました!
最後に、前回のモデル定義の際と同様に、Realmに対してactivity
プロパティを無視するように指定します。
class Santa: Object {
// Properties are all up here
// この関数は前回定義済みです。
override static func ignoredProperties() -> [String] {
// 前回定義した配列に "activity"を追加します。
return ["currentLocation", "activity"]
}
}
サンタクロースがいる場所の天気
この情報はRealm Object Serverの使い方を学習する際に扱います。現時点ではいったん気にしないでおきます。
配り終えていないプレゼントの数
このプロパティはそのままです。数なのでInt
で表現します!
class Santa: Object {
// いままでの諸々のプロパティはこの上
dynamic var presentsRemaining: Int = 0
// ignoredPropertiesはこの下
}
忘れずにdynamic
を指定するということ以外はとくに気をつけることはありません。
ダミーデータの追加
ここまででいくつかプロパティを追加したので、ダミーデータも更新しましょう。
extension Santa {
static func test() -> Santa {
let santa = Santa()
santa.currentLocation = Location(latitude: 37.7749, longitude: -122.4194)
santa.activity = .deliveringPresents
santa.presentsRemaining = 42
return santa
}
}
前回同様、ダミーデータは自由に変更してOKです。
アプリケーションを実行し、モデル定義が正しくコンパイルされることを確認してください。モデル定義をかなり変更したので、アプリケーションを実行するとRealmが「マイグレーションが必要」ということを意味する例外をスローします。ここでは、一旦アプリケーションを削除(実機の場合と同じくアイコンを長押しです)してから再度実行してください。UIに関するコードはいじっていないので、見た目は前回と変わっていないと思います。アプリが起動できたということは、Realmが新しいモデル定義を取り込んだということを意味します。
2. RealmのデータをUIに反映
前回、アプリケーションの起動時にサンタクロースの位置に応じてピンを立てるコードを書きました。今回は、より多くの情報をUIに反映させるとともに、データの変更に対してリアクティブにUIを更新するように拡張します。
-
まずは、接続しただけになっていたアウトレットを使っていきます。
SantaTrackerViewController
の中にupdate
メソッドを定義し、以下のようにSanta
クラスの情報からUIを更新する処理を記述してください。class SantaTrackerViewController: UIViewController { // Properties // viewDidLoad private func update(with santa: Santa) { mapManager.update(with: santa) let activity = santa.activity.description let presentsRemaining = "\(santa.presentsRemaining)" DispatchQueue.main.async { self.activityLabel.text = activity self.presentsRemainingLabel.text = presentsRemaining } } }
最初にサンタクロースの情報を前回作成したマップマネージャに渡して地図を更新させます。その後、activityとプレゼントの残数をラベルに反映します。前回と同様、
update(with:)
はメインスレッド以外から呼ばれることがあるため直接UIKitを操作するとクラッシュ💥します。解決策も前回と同様で、UIKit関連の処理は明示的にメインスレッドに処理をさせてください。到着時間については次回サンタクロースのルート情報をサーバーから受け取る際受け取る際に、天気についてはさらにその次のRealm Object Server���回に扱います。いまのところはUIの更新はこれでOKです。
-
次に、データの変更に対してリアクティブなコードに変えていきます。Realmはリアクティブパターンに従ってコードが書けるように設計されています。つまり、データの更新を起点にアプリが動作するということです。Realmを使ってリアクティブに記述する方法はいくつかあり詳細はドキュメントの「通知」の節で紹介されていますが、今回はKVOを使います。これは、KVOが唯一単一のオブジェクトに対して通知を受け取る方法であるという理由からです(2017年の初旬に、コレクション通知ベースの新しいAPIの導入も予定されています)。KVOのAPIは少し見た目がよくないので、今回は簡単なラッパーを用意しました。
class Santa: Object { // All of the existing code // これを保持しておかないと、通知が受け取れません private var observerTokens = [NSObject: NotificationToken]() // サンタクロースのプロパティ、およびプロパティのプロパティに対する変更の監視をセットアップします func addObserver(_ observer: NSObject) { // 必要なすべてのプロパティの変更通知をセットアップします // どれか一つでinitialを受け取る必要があります。どれでもかまいません addObserver(observer, forKeyPath: #keyPath(Santa._currentLocation), options: .initial, context: nil) // 位置情報の変更の通知をセットアップします addObserver(observer, forKeyPath: #keyPath(Santa._currentLocation.latitude), options: [], context: nil) addObserver(observer, forKeyPath: #keyPath(Santa._currentLocation.longitude), options: [], context: nil) addObserver(observer, forKeyPath: #keyPath(Santa._activity), options: [], context: nil) addObserver(observer, forKeyPath: #keyPath(Santa.presentsRemaining), options: [], context: nil) observerTokens[observer] = route.addNotificationBlock { // self owns this route, so it will always outlive this closure [unowned self, weak observer] changes in switch changes { case .initial: // 処理を簡単にするため、"route"に対するKVOの呼び出しを偽装します observer?.observeValue(forKeyPath: "route", of: self, change: nil, context: nil) case .update: observer?.observeValue(forKeyPath: "route", of: self, change: nil, context: nil) case .error: fatalError("Couldn't update Santa's info") } } } func removeObserver(_ observer: NSObject) { observerTokens[observer]?.stop() observerTokens.removeValue(forKey: observer) removeObserver(observer, forKeyPath: #keyPath(Santa._currentLocation)) removeObserver(observer, forKeyPath: #keyPath(Santa._currentLocation.latitude)) removeObserver(observer, forKeyPath: #keyPath(Santa._currentLocation.longitude)) removeObserver(observer, forKeyPath: #keyPath(Santa._activity)) removeObserver(observer, forKeyPath: #keyPath(Santa.presentsRemaining)) } }
このラッパーのおかげで通知を簡単に使うことができます。はじめに、通知不要なプロパティを除いたすべてのプロパティにオブザーバーを登録します。その際、
#keyPath
キーワードを使うことでコンパイラーがtypoがないことをチェックしてくれます。最後に、route
に対するRealmのコレクション通知をKVOの通知に偽装します。こうすることで、KVOの場合と同じコードで処理することができるようになります。 -
UIをリアクティブにするために必要なのは、このラッパーを使うことだけです。
SantaTrackerViewController
の中のviewDidLoad()
の最後の部分を書き換えましょう。override func viewDidLoad() { // 最後以外は前回のコードと同じです super.viewDidLoad() // MapManagerを準備 mapManager = MapManager(mapView: mapView) // Realmからサンタクロースの情報を取り出す let realm = try! Realm() let santas = realm.objects(Santa.self) // 情報が存在しない場合はダミー情報を書き込む if santas.isEmpty { try? realm.write { realm.add(Santa.test()) } } // 非オプショナルな型に変換 if let santa = santas.first { // 元々はMapManagerを呼び出すコードがありましたが、不要になりました! santa.addObserver(self) } }
変えたのは、MapManagerの
update(with:)
を呼ぶ代わりに自身をsanta
のオブザーバーとして登録する部分です。update(with:)
は変更が通知された際に呼び出されます。通知が不要になったらオブザーバーの登録を解除することを忘れないでください。今回はシンプルなアプリケーションなので解除を忘れても問題にはなりませんが、実際のアプリでは忘れずに行うべきです。以下はそのサンプルです。
deinit { let realm = try! Realm() let santas = realm.objects(Santa.self) if let santa = santas.first { santa.removeObserver(self) } }
独自の変更監視のセットアップと後始末を行いました。でもちょっとまってください。変更通知のハンドラーは?もちろんこれも書かないといけません。
viewDidLoad()
の下に、以下のKVOリスナーメソッドを新たに追加してください。override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if let santa = object as? Santa { update(with: santa) } else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } }
今回はKVOをとてもシンプルな使い方をしているので、引き数についてはほとんど気にする必要はありません。変更通知が
Santa
に対するものだということの確認だけはおこなってください。Santa
に対するものであればupdate(with:)
を呼び出し、そうでなければAppleのドキュメントに従い親クラスへ移譲しています。ここまでできたらアプリケーションを実行してみてください。ダミーデータがUIに反映されていると思います。もしうまく行っていたらおめでとうございます!新しモデル定義と、データの変更に対するリアクティブなUIを手に入れました。これで次回のRealm Object Server対応に向けて準備は完了です。Realm Object Serverの嬉しいところは、アプリケーションを対応させるための修正がほとんど不要であるというところです。UIをリアクティブにしてあるので、サーバーからの変更も今までのローカルでの変更とおなじように通知されます。そのため、サーバーからのデータを特別扱いする必要はありません。
いずれにしろ、次回をご覧いただけばはっきりします。それまではクリスマスショッピング🎁をお楽しみください!
About the content
This content has been published here with the express permission of the author.