RealmデータをwatchOSとiOSとの間でやりとりする(3)

#はじめに
([RealmデータをwatchOSとiOSとの間でやりとりする(2)](http://qiita.com/hsylife/items/03a84a0edca91976f938)の続きになります。)

watchOSとiOSとの間でRealmのデータをやりとりする実装方法について述べます。
watchOS2+、iOS9+、Swift 3を前提に話を進めます。
動作するコードを[サンプルプロジェクト](https://github.com/hsylife/SampleRealmOnWatchOS3)としてGitHubに公開しました。実機でランすれば、速度感を体験できます。

#WCSessionをアクティベートする
iOS/watchOS間でデータをやりとりするために、WatchConnectivityフレームワークの[WCSession](https://developer.apple.com/reference/watchconnectivity/wcsession)を使います。
最初に、iOS/watchOSのそれぞれでWCSessionをアクティベートします。
##iOS側の実装をする
アクティベートの成否などを取得できるdelegateメソッドを使いたいので、WCSessionDelegateを宣言します。

```.swift
class ViewController: UIViewController, WCSessionDelegate {
```

viewDidLoad()の中でアクティベートする処理を記述します。

```ViewController.swift
override func viewDidLoad() {
     super.viewDidLoad()
     if WCSession.isSupported() {
         let session = WCSession.default()
         session.delegate = self
         session.activate()
     }
}
```

これで、起動時にWCSessionをアクティベートできるようになりました。WCSessionDelegateの指定もしたので、deleagteメソッドが呼ばれるようになる準備も完了しました。

アクティベートが完了した場合に呼ばれる[session(_:activationDidCompleteWith:error:)](https://developer.apple.com/reference/watchconnectivity/wcsessiondelegate/1615679-session)はiOS9.3+なので、それ以前をターゲットに含める場合(iOS9.0を対象にしているときなど)は`@available(iOS 9.3, *)`をつける必要があります。

```ViewController.swift
@available(iOS 9.3, *)
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        print("activationDidComplete")
    }
```

##watchOS側を実装する
iOS側と同様にWCSessionDelegateを宣言します。

```InterfaceController.swift
class InterfaceController: WKInterfaceController, WCSessionDelegate {
```

起動時に処理したいので、awake()の中でアクティベートする処理を記述します。

```InterfaceController.swift
override func awake(withContext context: Any?) {
    super.awake(withContext: context)
    if WCSession.isSupported() {
      let session = WCSession.default()
      session.delegate = self
      session.activate()
    }
}
```

さきほどと同様に、[session(_:activationDidCompleteWith:error:)](https://developer.apple.com/reference/watchconnectivity/wcsessiondelegate/1615679-session)はwatchOS2.2+なので、`@available(watchOS 2.2, *)`をつける必要があります。

```InterfaceController.swift
@available(watchOS 2.2, *)
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
    print("activationDidComplete")
}
```


# ファイルを送信する
セッションをアクティベートできたので、今度は、iOS側のRealmファイルをwatchOS側へfileTransfer()を用いて送信します。

iOS側で送信したい時点でこの処理を行います。

```ViewController.swift
if let path = Realm.Configuration().fileURL {
    WCSession.default().transferFile(path, metadata: nil)
}
```

サンプルの中では、textFieldの変更時に毎回送信したいので、textFieldのrealmTextFieldEditingChanged()アクションにて、送信しています。加えて、iOS側アプリ起動時に一度送信するためにviewDidLoad()でも送信しています。

textFieldの編集毎に実行することにすると、短時間に大量のtransferFile()が実行される可能性があります。この点、[公式リファレンスの冒頭](https://developer.apple.com/reference/watchconnectivity/wcsession)で"those transfers happen opportunistically in the background."と書かれているように、実際は毎回送信が発生するわけではありません。

# ファイルを受信する
iOS側から送信されたファイルを、watchOS側で受信します。

InterfaceControllerは前述のとおりWCSessionDelegateを宣言していますので、そのdelegateメソッドであるsession(_ session: WCSession, didReceive file: WCSessionFile)を利用できるようになっています。

ファイルを受信した時に呼ばれるので、その中でRealmの標準ファイルを逐次、入れ替える処理を書きます。

サンプルでは、ファイルを入れ替えた後で、データを取得してUI系に渡しています。

```InterfaceController.swift
func session(_ session: WCSession, didReceive file: WCSessionFile) {
  var config = Realm.Configuration()
  config.fileURL = file.fileURL
  Realm.Configuration.defaultConfiguration = config
        
  let realm = try! Realm()
  if let firstField = realm.objects(Field.self).first{
    realmLabel.setText(firstField.text)
  }
}
```

これで、iOS側のtextFieldが変更される度にRealmデータベースの中にデータを格納し、そのファイル全体をWCSessionのtransferFileをしてiOS側からwatchOS側に送信して、これを受け取ったwatchOS側でRealmの標準ファイルを入れ替えた後、表示系を刷新できるようになりました。

[サンプルプロジェクト](https://github.com/hsylife/SampleRealmOnWatchOS3)としてGitHubに公開していますので、全体をもう一度ご確認ください。

#sendMessageとの比較
以上に見たのは、WCSesionが提供するバックグラウンド送信の一つtransferFile()を用いてRealmファイルを送信する方法でした。
パフォーマンスの比較のために、相互メッセージ(Interactive messaging)の方法として用意されているsendMessage()の方法もサンプルに実装しました。

sendMessage()は、[watchOS 2 の Watch Connectivity を使ってみた](http://qiita.com/koogawa/items/b39e1419cbbcda8efc07)に紹介されている通り、watchOS側がフォアグラウンド状態になっている必要がありますが、その場で送信できる方法です。

実装の流れとしては、次のようになります。
1.. WCSessionをアクティベートする。
2.. iOS側でsendMessage()にデータを持たせて送信を実行する。

```ViewController.swift
func sendMessage(){
        if WCSession.default().isReachable {
            let applicationDict = ["text": messageTextField.text ?? ""]
            WCSession.default().sendMessage(applicationDict,
                                            replyHandler: { replyDict in print(replyDict) },
                                            errorHandler: { error in print(error.localizedDescription)})
        }

    }
```

3.. watchOS側でWCSessionDelegateメソッドの`session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void)`が呼ばれるので、受信データを使ってしかるべき処理をする。

```InterfaceController.swift
  func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        
        if let text = message["text"] as! String?{
            messageLabel.setText(text)
        }
        
        replyHandler(["reply" : "OK"])
    }
```
ここでは、replyHandlerとして、`["reply" : "OK"]`を返す実装になっています。iOS側のViewController.swiftのsendMessage()のreplyHandler{}にこの値が渡るので、成功時にデバッガコンソールに`["reply" : "OK"]`の内容が表示されます。

#逆向きのデータ送信について
WCSessionはiOS→watchOSだけでなく、逆向きのwatchOS→iOSのデータ送信にも対応しています。ですので、watchOS側のRealm変更にiOS側を対応させることも可能です。

#関連リンク
[RealmデータをwatchOSとiOSとの間でやりとりする(1)](http://qiita.com/hsylife/items/c21f19b80c5c81f32fb8#_reference-f4ec6e57e8760250aef1)