12月に入りクリスマスが近づいてきていますが、わたしたちからもモバイル開発者のみなさんになにかプレゼントができないかと考えていました。みなさんが希望するものは何でしょう?🤔
素晴らしいアプリを開発することですよね! 🙌
そのためにわたしたちができることはRealmとしてすばらしいプロダクトをお届けすることです。そこでこの12月は、まだRealmを使っていない開発者の人たちに向けて新たなチュートリアルをお届けしようと思います。冬休みを待つ必要はありません。今すぐはじめましょう。
Swiftを使った全4回のチュートリアルで、クリスマスイブのサンタクロースを追跡するシンプルなアプリケーションを作成します。このチュートリアルでは、Realm Databaseデータベースによるデータの永続化や表示に加え、Realm Platformによるデータ同期を理解することができます。すでにRealmを使ったことがある方でも、きっと新たな発見があることでしょう。
チュートリアルで作るアプリはこのようなものです:
1回目はアプリ開発のための準備と、地図上に正しくサンタクロースの現在位置が表示されるようにするところまでの作業を行います。作業を8つのステップに分割したので、時間がある時に少しづつ進めていただいても問題ありません。
次回以降で、サンタクロースの飛行ルートと画面上のその他の情報の更新を行います。Realm Object Serverに関する回も予定しています。それでは始めましょう。
1. 新規Xcodeプロジェクトの作成
- “Single View Application”テンプレートを選択し、”Santa Tracker”という名前(別の名前でも問題ありませんが、以降”Santa Tracker”プロジェクトと呼びます)のプロジェクトを作成します。
- “Language”を”Swift”に、”Devices”を”iPhone”にします。もちろんCore Dataは必要ないので”Use Core Data”のチェックが外れていることを確認してください。
2. Storyboardの準備
- 今回作成するアプリケーションは1つのView Controllerのみで構成されます。あらかじめ用意したStoryboardファイルがあるので、このファイルからView Controllerをコピーしてください。
Main.storyboard
自体を置き換えてしまってもいいですし、中のView ControllerだけをInterface Builderでコピーしてもかまいません。 - IB上でコピーした場合は、右側のパネルで”Is Initial View Controller”にチェックを入れる必要があります。View Controllerの左側に対して、なにもないところから矢印が伸びてきていれば正しく設定できています。
- デザイン🎨を変えたい場合は自由にいじってください(とはいえAutoLayoutのデバッグにお付き合いすることはできないので、困ったときは私のLayoutをそのまま使ってください)。
- 下の方にある2つの画像は、プロジェクトに画像ファイルを追加するまで表示されません。画像ファイルはこちらから取得することができます。ダウンロードしたら
Assets.xcassets
に追加し、ImageViewの画像を正しくセットしてください(それぞれのviewでどの画像を使うべきかわかりますよね?)。 - 私のStoryboardをコピーした場合は、View Controllerクラス名を変更する必要があります。
ViewController.swift
ファイルを開き、クラス名をSantaTrackerViewController
に変えてください。この変更に合わせてファイル名を変えてもいいですが、必須ではありません(わたしは常にクラス名とファイル名を合わせるようにしています)。
3. MapKitをリンクする
- 言うまでもないことですが、地図を使う場合は(私のように)MapKitをリンクするのを忘れると起動時にアプリがクラッシュします。ちょっと恥ずかしいですね。
- プロジェクトの設定を開き、”General”タブの中の下の方にある”Linked Frameworks and Libraries”に
MapKit.framework
を追加します。
4. プロジェクトにRealmを追加する
- CocoaPodsを使ってRealm Swiftを追加します。CocoaPodsをインストールしていない場合は、先にインストールを行ってください。CocoaPods以外の方法でRealm Swiftを使いたい場合はこちらを参照してください。CocoaPodsが最も簡単です。
- あらかじめXcodeを終了しておきます。このあと新規にworkspaceを作成します。
- ターミナルを開き、プロジェクトのディレクトリへ移動してください。
.xcodeproj
があるところです。 -
pod init
コマンドを実行してPodfileを作成します。作成されたらファイルを開いてpod 'RealmSwift', '2.1.0'
を# Pods for Santa Tracker
と書かれた行のすぐ下に書いてください。保存してエディタを終了します。 - ターミナルへ戻り、
pod install
コマンドを実行してください。- CocoaPodsを初めて使用する場合、このコマンドは結構時間がかかります。お茶でものみながらゆっくりお待ち下さい。🍵
-
Unable to satisfy the following requirements
と書かれた大量のテキストが表示された場合は、Podspecファイルが古いです。pod repo update
コマンドを実行して更新する必要があります(こちらも時間がかかります🍵)。 -
Pod installation complete!
と表示されれば完了です!
- CocoaPodsを使うためには、
.xcworkspace
ファイルを開く必要があります。.xcodeproj
ファイルではないのでご注意ください。.xcodeproj
を開いた場合はCocoaPods関連のファイルにプロジェクトからアクセスできません。
5. アプリを実行する(ここまでの手順の確認のため)
-
.xcworkspace
ファイルを開き、Run(playボタン)を押します。▶️ - シミュレータが起動しアプリが実行されればここまでの手順は完璧ということです 🎉
- もしそうならなければ、デバッグが必要です。ここまでのステップを_すべて_実行しているか確認してください。もしサポートが必要な場合はSlackかTwitterでお知らせください。自分でデバッグしてみてどうしてもわからない場合だけにしてくださいね。
6. Outletを設定する
以下のOutletをSantaTrackerViewController
にセットアップします:
@IBOutlet private weak var timeRemainingLabel: UILabel!
@IBOutlet private weak var mapView: MKMapView!
@IBOutlet private weak var activityLabel: UILabel!
@IBOutlet private weak var temperatureLabel: UILabel!
@IBOutlet private weak var presentsRemainingLabel: UILabel!
フィールドを追加したあと、IB上のViewとの接続を行ってください。どこに繋げばいいかは名前からわかると思います。
MKMapView
の参照を解決するために、import MapKit
の記述を追加してください。
7. データモデルの定義
モデルクラスの定義を行う前に注意点があります。モデル定義では、必ず(もう一度いいます、必ずです)プロパティとモデルの名前を以下の定義と同じにしてください。そうなっていないと、あとの回でRealm Object Serverに接続した際にアプリがクラッシュして悲しい思いをします。
悲しむ必要はありません。必要なのはプロパティの名前を合わせることです。
-
サンタクロースのためのデータモデルを定義していきましょう。モデル定義は段々と成長させていけば良いので、位置情報を表示するために最低限必要な情報を定義することから始めます。サンタクロース(Santa)のための空のSwiftファイルを作成します。ファイル名はもちろん
Santa.swift
です。Santa
オブジェクトは一つしか使いませんが、それでもデータの同期のためにはクラスとして定義を行う必要があります。class Santa: Object { dynamic var currentLocation: Location? }
モデル定義のファイルには、
import RealmSwift
を書くのを忘れないでください。Santa
クラスはサンタクロースの現在位置のために、Location?
型のcurrentLocation
プロパティを持っています。「Location?
って何?」という声が聞こえてきます(あなたからもSwiftコンパイラからも)。 -
エラーを解消するために、別のSwiftファイル
Location.swift
を作成します。class Location: Object { dynamic var latitude: Double = 0.0 dynamic var longitude: Double = 0.0 convenience init(latitude: Double, longitude: Double) { self.init() self.latitude = latitude self.longitude = longitude } }
このクラスは、同期可能な形で位置を表現します。とてもシンプルな構造で、
latitude
(緯度)とlongitude
(経度)のためのプロパティとあとで使うconvenience initializerを持っています。 -
これで
Santa.swift
のエラーは解消されたはずです。ここで、先に進む前にもうひとつあとで楽をするための一手間をかけておきます。currentLocation
がオプショナルであることにお気づきでしょうか。このプロパティを使うところでいちいちオプショナルをunwrapするコードは書きたくありません。特にはじめにサンタクロースの初期値を保存する今回のような場合はとくにそうです。これを回避するため、Santa.swif
を以下のように書き換えます。class Santa: Object { private dynamic var _currentLocation: Location? var currentLocation: Location { get { // サンタクロースの位置が不明な場合は、おそらくまだ自宅です。 return _currentLocation ?? Location(latitude: 90, longitude: 180) } set { _currentLocation = newValue } } override static func ignoredProperties() -> [String] { return ["currentLocation"] } }
Santa
の外部に対するAPIは変わっていません。currentLocation
プロパティが一つ見えるだけです。ただし、その型はオプショナルではなくなっています👏。Realm Swiftドキュメントには、Object
プロパティ(つまり型がRealmオブジェクトであるもの)は”オプショナル_でなければならない_“と書かれています。そのため、通常のSwiftのやりかたで初期値を与えたり非オプショナルにすることはできません。代わりに、実際に値を保持するprivate
なフィールドを用意し、外部から見えるダミーのプロパティを非オプショナルにし、これをignoredProperties()
を使ってRealmには無視させます(read-onlyなプロパティは自動的に無視されるという仕様もありますが、今回はセッターも必要なので使えません)。 -
テストの際に便利なように、デモ用のデータを作るための関数も追加しておきます。
extension Santa { static func test() -> Santa { let santa = Santa() santa.currentLocation = Location(latitude: 37.7749, longitude: -122.4194) return santa } }
ここではサンタクロースはサンフランシスコにいるとしています。もしあなたのところにいるようにしたければ、こちらで座標を取得してください。
補足: 経度(latitude)と緯度(longitude)を間違えないようにしてください。このチュートリアルを執筆する、私は何度も(何度も[何度も])間違えました。ここに、役に立つ図があります。もしピンが意図したところに立たない場合は、このことを思い出してください。
8. サンタクロースの位置を地図に表示する!
-
地図はさまざまな機能を実現するための部品として提供されているので、それを扱う専用のクラスを作ります。
MapManager.swift
という名前で新規にSwiftファイルを作成し、中に以下のように記述します。class MapManager: NSObject { private let santaAnnotation = MKPointAnnotation() init(mapView: MKMapView) { santaAnnotation.title = "🎅" super.init() mapView.addAnnotation(self.santaAnnotation) } func update(with santa: Santa) { // サンタクロースの位置を表示するために地図を更新 self.santaAnnotation.coordinate = santaLocation } }
コンパイラが地図関連のクラスについて理解できるようにするため、
import MapKit
が必要です。MapManager
はMKPointAnnotation
を使って地図上の一点を見慣れたピン📍のUIで示します。MapManager
を作成する際、ピンに対するアノテーションを作成し、地図に追加しています。 -
それではピンがどこに立つべきかや、どのタイミングでそれを更新するのでしょうか。これらは
MapManager
の責務ではありません。必要に応じて外部からサンタクロースの情報を受け取ります。ここでの責務は、ピンの座標を指定された新しい位置に更新するということのみです。ここでひとつ問題があります。サンタクロースの位置情報は
Location
ですが、MKPointAnnotation
が期待する型はCLLocationCoordinate2D
です。それに、もうとつ微妙なバグがあります。それは、update(with:)
がメインスレッド以外から呼び出されるとアプリのクラッシュを引き起こすということです。というのも、現在位置の変更はUIKit呼び出しを伴うからです。現時点ではこのバグが顕在化することはありませんが、対応していないと以降の回で問題になります。ひとつめの問題から片付けましょう。
Location
をCLLocationCoordinate2D
に変換するのは簡単です。MapManager
に、Location
に対する以下のようなextensionを追加しましょう。private extension Location { var clLocationCoordinate2D: CLLocationCoordinate2D { return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) } }
UIColor.cgColor
の流儀に従って名前をつけました。単に2つの値をそのまま渡しているだけです。簡単ですね。このextensionはMapManager.swift
に追加する必要が有ることに注意してください。というのも、このextensionはMapManager
クラスからしか使われないからです。また、こうすることでLocation.swift
でimport CoreLocation
する必要もなくなります。スレッドの問題を修正するも簡単です。
coordinate
の更新をDispatch.main.async
の中で行うようにするだけです。これらの問題をまとめて解決するため、
update(with:)
を以下のように更新してください。func update(with santa: Santa) { let santaLocation = santa.currentLocation.clLocationCoordinate2D DispatchQueue.main.async { self.santaAnnotation.coordinate = santaLocation } }
Santa
オブジェクトを作成したスレッド以外からSanta
オブジェクトを利用することはできないため(詳細はドキュメントを見てください)、メインスレッドに直接Santa
オブジェクトを渡すことはできません。そこで、事前に取り出した値を渡しています。 -
それではマネージャにサンタクロースの位置を教えましょう。
SantaTrackerViewController
を以下のように更新してください。// Has to be implicitly unwrapped // Needs the reference to the map view private var mapManager: MapManager! 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 { // Update the map mapManager.update(with: santa) } }
ここではRealmのモデルを扱っているため、ファイルの先頭に
import RealmSwift
が必要です。MapManager
を準備していますが、地図のviewを参照する必要があるため暗黙的なunwrapをしています(この問題に対処するにはもっと良い方法があることは知っていますが、私は霊能力🔮によりこちらのほうが以降の回でリファクタリングの影響を最小限に留めることができることがわかっているのでここではこのようにしています)。そして、Realmを開き初回起動時のためにテストデータを用意したらサンタクロースの位置を更新します。以降の回でRealmのエラーにもしっかり対処するよう変更しますが、いまのところはエラーを無視するようにしておきます。
もしうまくいかない場合は、以下の点を確認してみてください(これで全てではありませんが、すくなくとも私自身が経験したものをあげておきます)。
- 正しくフレームワークやファイルを
import
していますか? - すべての
@IBOutlet
を接続しましたか? - これまでのステップをもれなく実行していますか?特にコードを手打ちした場合には間違えることがよくあります。
アプリケーションを実行してピンが期待した場所に表示されているでしょうか。おめでとうございます!これで(第1回は)完了です!
- 正しくフレームワークやファイルを
第2回を公開しました。ぜひご覧ください!
About the content
This content has been published here with the express permission of the author.