Track Santa with Realm: Part 1

12月に入りクリスマスが近づいてきていますが、わたしたちからもモバイル開発者のみなさんになにかプレゼントができないかと考えていました。みなさんが希望するものは何でしょう?🤔

素晴らしいアプリを開発することですよね! 🙌

そのためにわたしたちができることはRealmとしてすばらしいプロダクトをお届けすることです。そこでこの12月は、まだRealmを使っていない開発者の人たちに向けて新たなチュートリアルをお届けしようと思います。冬休みを待つ必要はありません。今すぐはじめましょう。

Swiftを使った全4回のチュートリアルで、クリスマスイブのサンタクロースを追跡するシンプルなアプリケーションを作成します。このチュートリアルでは、Realm Databaseデータベースによるデータの永続化や表示に加え、Realm Platformによるデータ同期を理解することができます。すでにRealmを使ったことがある方でも、きっと新たな発見があることでしょう。

チュートリアルで作るアプリはこのようなものです:

The finished Santa tracking app

1回目はアプリ開発のための準備と、地図上に正しくサンタクロースの現在位置が表示されるようにするところまでの作業を行います。作業を8つのステップに分割したので、時間がある時に少しづつ進めていただいても問題ありません。

次回以降で、サンタクロースの飛行ルートと画面上のその他の情報の更新を行います。Realm Object Serverに関する回も予定しています。それでは始めましょう。

1. 新規Xcodeプロジェクトの作成

  1. “Single View Application”テンプレートを選択し、”Santa Tracker”という名前(別の名前でも問題ありませんが、以降”Santa Tracker”プロジェクトと呼びます)のプロジェクトを作成します。
  2. “Language”を”Swift”に、”Devices”を”iPhone”にします。もちろんCore Dataは必要ないので”Use Core Data”のチェックが外れていることを確認してください。

2. Storyboardの準備

  1. 今回作成するアプリケーションは1つのView Controllerのみで構成されます。あらかじめ用意したStoryboardファイルがあるので、このファイルからView Controllerをコピーしてください。Main.storyboard自体を置き換えてしまってもいいですし、中のView ControllerだけをInterface Builderでコピーしてもかまいません。
  2. IB上でコピーした場合は、右側のパネルで”Is Initial View Controller”にチェックを入れる必要があります。View Controllerの左側に対して、なにもないところから矢印が伸びてきていれば正しく設定できています。
  3. デザイン🎨を変えたい場合は自由にいじってください(とはいえAutoLayoutのデバッグにお付き合いすることはできないので、困ったときは私のLayoutをそのまま使ってください)。
  4. 下の方にある2つの画像は、プロジェクトに画像ファイルを追加するまで表示されません。画像ファイルはこちらから取得することができます。ダウンロードしたらAssets.xcassetsに追加し、ImageViewの画像を正しくセットしてください(それぞれのviewでどの画像を使うべきかわかりますよね?)。
  5. 私のStoryboardをコピーした場合は、View Controllerクラス名を変更する必要があります。ViewController.swiftファイルを開き、クラス名をSantaTrackerViewControllerに変えてください。この変更に合わせてファイル名を変えてもいいですが、必須ではありません(わたしは常にクラス名とファイル名を合わせるようにしています)。

3. MapKitをリンクする

  1. 言うまでもないことですが、地図を使う場合は(私のように)MapKitをリンクするのを忘れると起動時にアプリがクラッシュします。ちょっと恥ずかしいですね。
  2. プロジェクトの設定を開き、”General”タブの中の下の方にある”Linked Frameworks and Libraries”にMapKit.frameworkを追加します。

4. プロジェクトにRealmを追加する

  1. CocoaPodsを使ってRealm Swiftを追加します。CocoaPodsをインストールしていない場合は、先にインストールを行ってください。CocoaPods以外の方法でRealm Swiftを使いたい場合はこちらを参照してください。CocoaPodsが最も簡単です。
  2. あらかじめXcodeを終了しておきます。このあと新規にworkspaceを作成します。
  3. ターミナルを開き、プロジェクトのディレクトリへ移動してください。.xcodeprojがあるところです。
  4. pod initコマンドを実行してPodfileを作成します。作成されたらファイルを開いてpod 'RealmSwift', '2.1.0'# Pods for Santa Trackerと書かれた行のすぐ下に書いてください。保存してエディタを終了します。
  5. ターミナルへ戻り、pod installコマンドを実行してください。
    • CocoaPodsを初めて使用する場合、このコマンドは結構時間がかかります。お茶でものみながらゆっくりお待ち下さい。🍵
    • Unable to satisfy the following requirementsと書かれた大量のテキストが表示された場合は、Podspecファイルが古いです。pod repo updateコマンドを実行して更新する必要があります(こちらも時間がかかります🍵)。
    • Pod installation complete!と表示されれば完了です!
  6. CocoaPodsを使うためには、.xcworkspaceファイルを開く必要があります。.xcodeprojファイルではないのでご注意ください。.xcodeprojを開いた場合はCocoaPods関連のファイルにプロジェクトからアクセスできません。

5. アプリを実行する(ここまでの手順の確認のため)

  1. .xcworkspaceファイルを開き、Run(playボタン)を押します。▶️
  2. シミュレータが起動しアプリが実行されればここまでの手順は完璧ということです 🎉
  3. もしそうならなければ、デバッグが必要です。ここまでのステップを_すべて_実行しているか確認してください。もしサポートが必要な場合はSlackTwitterでお知らせください。自分でデバッグしてみてどうしてもわからない場合だけにしてくださいね。

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に接続した際にアプリがクラッシュして悲しい思いをします。

悲しむ必要はありません。必要なのはプロパティの名前を合わせることです。

  1. サンタクロースのためのデータモデルを定義していきましょう。モデル定義は段々と成長させていけば良いので、位置情報を表示するために最低限必要な情報を定義することから始めます。サンタクロース(Santa)のための空のSwiftファイルを作成します。ファイル名はもちろんSanta.swiftです。Santaオブジェクトは一つしか使いませんが、それでもデータの同期のためにはクラスとして定義を行う必要があります。

    class Santa: Object {
        dynamic var currentLocation: Location?
    }
    

    モデル定義のファイルには、import RealmSwiftを書くのを忘れないでください。Santaクラスはサンタクロースの現在位置のために、Location?型のcurrentLocationプロパティを持っています。「Location?って何?」という声が聞こえてきます(あなたからもSwiftコンパイラからも)。

  2. エラーを解消するために、別の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を持っています。

  3. これで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なプロパティは自動的に無視されるという仕様もありますが、今回はセッターも必要なので使えません)。

  4. テストの際に便利なように、デモ用のデータを作るための関数も追加しておきます。

    extension Santa {
        static func test() -> Santa {
            let santa = Santa()
            santa.currentLocation = Location(latitude: 37.7749, longitude: -122.4194)
            return santa
        }
    }
    

    ここではサンタクロースはサンフランシスコにいるとしています。もしあなたのところにいるようにしたければ、こちらで座標を取得してください。

    補足: 経度(latitude)と緯度(longitude)を間違えないようにしてください。このチュートリアルを執筆する、私は何度も(何度も[何度も])間違えました。ここに、役に立つ図があります。もしピンが意図したところに立たない場合は、このことを思い出してください。

8. サンタクロースの位置を地図に表示する!

  1. 地図はさまざまな機能を実現するための部品として提供されているので、それを扱う専用のクラスを作ります。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が必要です。MapManagerMKPointAnnotationを使って地図上の一点を見慣れたピン📍のUIで示します。MapManagerを作成する際、ピンに対するアノテーションを作成し、地図に追加しています。

  2. それではピンがどこに立つべきかや、どのタイミングでそれを更新するのでしょうか。これらはMapManagerの責務ではありません。必要に応じて外部からサンタクロースの情報を受け取ります。ここでの責務は、ピンの座標を指定された新しい位置に更新するということのみです。

    ここでひとつ問題があります。サンタクロースの位置情報はLocationですが、MKPointAnnotationが期待する型はCLLocationCoordinate2Dです。それに、もうとつ微妙なバグがあります。それは、update(with:)がメインスレッド以外から呼び出されるとアプリのクラッシュを引き起こすということです。というのも、現在位置の変更はUIKit呼び出しを伴うからです。現時点ではこのバグが顕在化することはありませんが、対応していないと以降の回で問題になります。

    ひとつめの問題から片付けましょう。LocationCLLocationCoordinate2Dに変換するのは簡単です。MapManagerに、Locationに対する以下のようなextensionを追加しましょう。

       private extension Location {
           var clLocationCoordinate2D: CLLocationCoordinate2D {
               return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
           }
       }
    

    UIColor.cgColorの流儀に従って名前をつけました。単に2つの値をそのまま渡しているだけです。簡単ですね。このextensionはMapManager.swiftに追加する必要が有ることに注意してください。というのも、このextensionはMapManagerクラスからしか使われないからです。また、こうすることでLocation.swiftimport 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オブジェクトを渡すことはできません。そこで、事前に取り出した値を渡しています。

  3. それではマネージャにサンタクロースの位置を教えましょう。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.


Michael Helmbrecht

Michael designs and builds things: apps, websites, jigsaw puzzles. He’s strongest where disciplines meet, and is excited to bring odd ideas to the table. But mostly he’s happy to exchange knowledge and ideas with people. Find him at your local meetup or ice cream shop, and trade puns.

4 design patterns for a RESTless mobile integration »

close