Apple Watch (Watch OS4)
この記事では、Apple Watch (以降Watch OS)の開発にあたっての私なりの開発メモを記します。
また、何の断りがなければ、Watch OSはWatch OS4の事を指しています。
環境としては、
- XCode 9.4.x
- Watch OS 4 (Watch Kit2)
での開発です。
特にCoreData等のリソース周りの資料があまり見つからなかったので、そこを重点的に記載しています。
また、今回の記事はTIME HACKERでApple Watch対応を行っている時に、主に私が悩んだ所ですので、CoreDataとのデータのやり取りを主にフォーカスしています。
- Apple Watch (Watch OS4)
- これまでのWatch OS (初代Watch Kit)との違い
- 新しいWatch OSでのCoreData等のリソースアクセスの考え方 Watch Connectivity
- データ送受信部分
- Watch OSとiPhoneとでCoreData等のデータのやり取りを行います。
- 詰まった所
- 最後に
これまでのWatch OS (初代Watch Kit)との違い
Watch OS開発は、これまで(初代Watch Kit)とは、特にリソースの取扱方法が異なってます。
その違いは、以下のAppleが展開している図を見ると明らかです。
これまでは、Apple WatchはiPhone上で動作させること前提の仕組みとなっていた為、CoreDataやUserDefaultsをApp Groupsの設定さえ行っていれば、共有リソースとしてアクセスすることが出来ました。
しかし、Watch Kit 2からは、Apple Watch上で独立して動作する事になり、
Apple Watchから共有リソースへのアクセスができなくなりました。
新しいWatch OSでのCoreData等のリソースアクセスの考え方 Watch Connectivity
UserDefaults等で取り扱う、一時的な設定等は、Apple Watchでそのまま使えば良いでしょう。
しかし、iPhone上のアプリとデータを共有する場合は、Apple Watch上のアプリからiPhoneアプリへデータの送受信を行うと言う考え方で取り扱う事になります。
このときに、Watch Connectivityを用いて送受信を行います。
図で言う、枠で囲った所がそれに当たります。
しかし、これには注意点があります。
これまでは単純に同一デバイス内のリソースファイルへアクセスしていたので、それなりに高速に取り扱うことが出来ましたが。
Apple WatchとiPhoneとは物理的にも分断されている通り、Sessionでのデータのやり取りになります。
当然、画像ファイル等のやり取りを行うと、その分データの送受信に時間がかかるので注意が必要です。
画像ファイルをWebから取得して利用するのではなく、予めアプリに組み込んだものを利用するのであれば、iPhoneとApple Watch両方に同一画像を埋め込んでおくのが妥当だと思います。
では、実際にiPhoneとWatch OSとでリソースのやり取りを行うサンプルを造っていきます。
データ送受信部分
まずは、データの送受信を行う所を造ります。
イメージは、繰り返しになりますが、以下の図の用に考えます。
iPhone, Watch OS共にデータの送受信を行う為のSessionをこさえます。
iPhone App側の実装
以下のようにWatchManagerクラスを用意します。
WatchManagerは以下の役割を担います。
- Watch OSとのSession
- Watch OSから来たEventの種別を判定 (後述)
また、WatchManagerはSessionを取り扱う為、念の為シングルトン設計にしています。
import WatchConnectivity class WatchManager: NSObject, WCSessionDelegate { let wcSession = WCSession.default private static let instance = WatchManager() public class var shared: WatchManager { return instance } func initWCSession() { if WCSession.isSupported() { wcSession.delegate = self wcSession.activate() print("[INFO] session activate") } else { print("[INFO] session error") } } } extension WatchManager { /// メッセージを受信した時の処理 /// /// - Parameters: /// - session: /// - message: /// - replyHandler: func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { var result: [String: Any] = ["success" : false] replyHandler(result) } } /// Session Delegate extension WatchManager { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { print("[INFO] The session has completed activation.") } func sessionDidBecomeInactive(_ session: WCSession) { print("[INFO] The session has got into inactivation.") } func sessionDidDeactivate(_ session: WCSession) { print("[INFO] The session has deactivated") } func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) { print(userInfo) } func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { print("[INFO] try to send message to watch") sendMessage() } }
やっていることは、WCSessionオブジェクトを生成し、delegateを自分自身にしているだけです。
これを例えば、AppDelegate等で
class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { WatchManager.shared.initWCSession() } }
と、呼んであげるとWatch OS用のSessionを貼ることが出来ます。
後は、Watch OSからEventが飛んでくると、適宜Watch Manager内のDelegateが呼び出されます。
上記サンプルで
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { }
このコードだけ、中身に軽い処理を入れているのは、今回のサンプルでは、主にこのDelegateを用いる為です。念の為。
Watch OS側の実装
iPhone App側の実装は出来たので、Watch OS側の実装を行います。
今回のサンプルでは、Watch OSが起動した際に、iPhone AppへSessionを開始するコードとなっています(この際はloadDataメソッドをコールします)。
import WatchKit import Foundation import WatchConnectivity class InterfaceController: WKInterfaceController, WCSessionDelegate { let wcSession = WCSession.default override func awake(withContext context: Any?) { super.awake(withContext: context) // WCSessionの開始 if WCSession.isSupported() { wcSession.delegate = self wcSession.activate() } } override func didAppear() { super.didAppear() print("[Info] didAppear") if wcSession.isReachable { print("[Info] reachable") loadData() } else { print("[Info] not reachable") } } override func willActivate() { super.willActivate() } override func didDeactivate() { super.didDeactivate() } func loadData() { let message = [ "message" : "test"] wcSession.sendMessage(message, replyHandler: { replyDict in print("[INFO] responce action data") print(replyDict) }, errorHandler: { error in print(error) }) } } // MARK: - Session Delegate extension InterfaceController { func sessionReachabilityDidChange(_ session: WCSession) { print("[Info] sessionReachabilityDidChange") if wcSession.isReachable { print("[Info] reachable") loadData() } else { print("[Info] not reachable") } } func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { print("[Info] The session has completed activation.") } func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { // iPhoneからのデータを受け取る print("[Info] didReceiveMessage") } }
上記コードの、
wcSession.sendMessage(message, replyHandler: { replyDict in print("[INFO] responce action data") print(replyDict) }, errorHandler: { error in print(error) })
この部分で、iPhone App側に通信を送っています。
ここで、例えばWatchManagerのメソッドを以下のように変更します。
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { print(message) var result: [String: Any] = ["success" : false] replyHandler(result) }
これで実行すると、 以下の情報が出力されます。
[ "message" : "test"]
ここまでで、iPhone AppとWatch OS Appとの接続の部分が出来ました
Watch OSとiPhoneとでCoreData等のデータのやり取りを行います。
ここに来て、やっと表題のお話が出来ます…。
これまでは、Watch OS AppとiPhone Appとの通信部分を造ってきたので、
この流れの上で、リソースデータのやり取りを行います。
といっても、設計はごく単純で、
すべてのデータのやり取りはiPhone側で行い、Watch OS側は適宜データの送受信のEventを送るだけです。
最初の方に載せた図で言うと、以下のようにApple Watchから来たEventのレシーバからCoreDataへアクセスし、データが必要であれば、返却する仕組みです。
前提として
今回のデータの送受信を行うにあたり、以下のデータ構造をDictionary形式でやり取りします。
Apple Watchからデータを取得するリクエストを投げる時
[ "kind" : 1, ]
kind : 1はデータ取得を行いたい識別子です。
Apple Watchからデータを更新した際にリクエストを投げる時
[ "kind" : 2, "data" : Any ]
この場合は、kind : 2として、一緒に更新したいデータを送信します。
では、コードの方を見てみます。
iPhone App側の実装
これまで書いてきた、WatchManagerのメソッドを以下のように変更します。
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { var result: [String: Any] = ["success" : false] guard let kind = message["kind"] as? Int else { result["success"] = false replyHandler(result) return } switch kind { case 1: result["data"] = /* CoreDataからとってきたデータ */ result["success"] = true return case 2: /* CoreDataのデータ更新 */ result["success"] = true return default: replyHandler(result) return } }
kindで、処理の識別を行い、適宜CoreDataの操作を行います。
最後に結果をreplyHandlerに渡して上げるだけです。
Watch OS側の実装
iPhone Appでデータを受信して、適切なレスポンスを返せるようになったので、
今度はWatch OS側からEventを送信する方法です。
まずは、データを受け取る所ですが、
loadDataメソッドを以下のように改変します。
func loadData() { let message = [ "kind" : 1] wcSession.sendMessage(message, replyHandler: { replyDict in print("[INFO] responce action data") print(replyDict) /* replyDictにCoreDataから取得した情報が入っているので、以後適切な処理を行う */ }, errorHandler: { error in print(error) }) }
データ受信の際は、kind : 1でSession通信を行えば、iPhone側でCoreDataにアクセスし、データを返却してくれます。
次に、データ更新は、上記loadDataと殆ど同じですが、念の為載せておきます。
データ更新ように、saveDataメソッドを追加しています。
func saveData() { var message: [String: Any] = [:] message["kind"] = 2 message["data"] = data wcSession.sendMessage(message, replyHandler: { replyDict in print(replyDict) }, errorHandler: { error in print(error) }) }
これで、適切なタイミングでsaveDataを呼び出せば、保存したいデータをCoreDataに保存することが出来ます。
詰まった所
終わりに、私が盛大に詰まった所として…。
当たり前のように、iPhone AppのWatch ManagerでreplyHandlerを使っていますが、
Apple WatchでSession処理を行う際に、適切なDelegateをiPhone App側で実装していないと通信エラーとなります。
この詳細は、以下にまとめておきましたので、もし詰まってしまった場合の参考にしてください。
最後に
Watch OSは他のWidget等と違って、これまでのWidget等と違って、考え方を大きくシフトしないとなかなか造りにくいです。
しかし、WebAPIを用いた実装を行っているんだと、考えると一気に考え方が楽になります。(Appleはここまで考えているんでしょうね・・・さすがや)
最後に宣伝となりますが、
この記事は、現在TIME HACKERでApple Watch対応を行っており、その際の備忘録として記したものです。
もう少しでリリースが見えている所ですので、是非とも皆さんお使いになってくださいね!