[watchOS 3] Watch Connectivity のデータ受け取りをバックグラウンドで行う

2016.11.04

はじめに

こんにちは。モバイルアプリサービス部の平屋です。

前回の記事「[watchOS 3] WKSnapshotRefreshBackgroundTask を使用して watchOS アプリの UI 更新する」に引き続き、watchOS 3.0 で追加されたバックグラウンド実行用のクラスについての情報を紹介していきます。

本記事では watchOS 3.0 で追加された WKWatchConnectivityRefreshBackgroundTask クラスの使用方法を紹介します。

このクラスは watchOS 2 で追加された機能「Watch Connectivity」に関連します。

検証環境

  • Xcode Version 8.1
  • iPhone 7, iOS 10.1
  • Apple Watch, watchOS 3.1

目次

Watch Connectivity とは

Watch Connectivity は watchOS アプリと iOS アプリ間で情報をやりとりするための機能であり、watchOS 2 で追加されました。

Watch Connectivity には「Background transfers」と「Interactive messaging」の 2 つのカテゴリが存在します。データのやりとりをリアルタイムで行いたい場合は Interactive messaging を、そうでない場合は Background transfers を使用します。

  • Background transfers
    • Application context
    • User info transfer
    • Complication data transfer
    • File transfer
  • Interactive messaging

watchOS 3.0 での変更点

watchOS 3.0 では Background transfers のためのクラス WKWatchConnectivityRefreshBackgroundTask が追加されました。このクラスは、データの受け取りをバックグラウンドで行う場合に使用するクラスです。

本記事では Background transfers の User info transfer を使用して iOS アプリから watchOS アプリへデータを転送し、watchOS アプリ側でのデータの受け取りをバックグラウンドで行う手順を解説していきます。

[1] セッションをアクティブにする

まずは、iOS アプリと watchOS アプリ間のセッションをアクティブにします。

セッションの delegate 設定とアクティベーション

任意のタイミングで、WCSessiondelegate プロパティにオブジェクトを割り当て、activate() メソッドを呼びます。

iOS

class ViewController: UITableViewController {

    // ...

    override func viewDidLoad() {
        super.viewDidLoad()

        if (WCSession.isSupported()) {
            // デバイスが Watch Connectivity に対応してる場合

            let session = WCSession.default()
            session.delegate = self
            session.activate()
        }
    }

    // ...
}

watchOS

class InterfaceController: WKInterfaceController {

    // ...

    override func awake(withContext context: Any?) {
        super.awake(withContext: context)

        if (WCSession.isSupported()) {
            // デバイスが Watch Connectivity に対応してる場合

            let session = WCSession.default()
            session.delegate = self
            session.activate()
        }
    }

    // ...
}

実装必須の delegate メソッドを実装

WCSessionDelegate のメソッドのうち、実装必須のメソッドを実装します。

セッションのアクティベーションが完了すると、session(_:activationDidCompleteWith:error:) メソッドが呼ばれます。

iOS

以下のメソッドの実装が必須です。

  • session(_:activationDidCompleteWith:error:)
  • sessionDidBecomeInactive(_:)
  • sessionDidDeactivate(_:)

watchOS

以下のメソッドの実装が必須です。

  • session(_:activationDidCompleteWith:error:)

[2] データを送信する

User info transfer を使用して iOS から watchOS へデータを送信します。

WCSessiontransferUserInfo(_:) メソッドを使用すると、WKWatchConnectivityRefreshBackgroundTask の生成がトリガーされます。

iOS

// MARK: - UITableViewDelegate
extension ViewController {
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // ここでは、色を指定する文字列を送信する
        let color = self.rows[indexPath.row]
        WCSession.default().transferUserInfo(["color" : color])
    }
}

[3] タスクを受け取る

手順 2 によって生成された WKWatchConnectivityRefreshBackgroundTaskWKExtensionDelegatehandle(_ backgroundTasks: Set) メソッドで取得可能です。このメソッドは、前回、前々回の記事でも使用したメソッドです。

ここで取得できる WKWatchConnectivityRefreshBackgroundTask は、データの転送処理が終わるまで、参照を保持しておきます。

watchOS

class InterfaceController: WKInterfaceController {

    // ...

    fileprivate var wcBackgroundTasks: [WKWatchConnectivityRefreshBackgroundTask] = []

    // ...
}

extension InterfaceController: WKExtensionDelegate {
    func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
        for task in backgroundTasks {
            if let task = task as? WKWatchConnectivityRefreshBackgroundTask {
                // タスクが完了するまで、WKWatchConnectivityRefreshBackgroundTask への参照を保持しておく
                // タスクの保持は開発者の責務 (WWDC 16 のセッション 218 で言及)
                self.wcBackgroundTasks.append(task)
            }
        }
    }
}

[4] データを受け取る

受信したデータは WCSessionDelegatesession(_:didReceiveUserInfo:) メソッドで取得できます。

watchOS

extension InterfaceController: WCSessionDelegate {
    func session(_ session: WCSession,
                 didReceiveUserInfo userInfo: [String : Any] = [:]) {
        // データを取り出す
        guard let color = userInfo["color"] as? String else { return }

        // watchOS アプリにデータを反映する処理
        DispatchQueue.main.async(execute: { () in
            self.label.setText("■■■ \(color) ■■■")
            self.label.setTextColor(self.colorMapping[color])
        })
    }

    // ...
}

[5] タスクの完了を OS に伝える

awake(withContext:) メソッド内などで WCSessionhasContentPending プロパティの変更の監視を開始しておきます。

データの転送が終わると hasContentPending プロパティの値が false になるので、そのタイミングでタスクを完了させます。

watchOS

class InterfaceController: WKInterfaceController {

    // ...

    override func awake(withContext context: Any?) {
        super.awake(withContext: context)

        // ...

        WCSession.default().addObserver(self,
                                        forKeyPath: "hasContentPending",
                                        options: [],
                                        context: nil)
    }

    // ...

    override func observeValue(forKeyPath keyPath: String?,
                               of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?,
                               context: UnsafeMutableRawPointer?) {

        DispatchQueue.main.async {
            let session = WCSession.default()

            // データの転送が終わると、hasContentPending は false になる
            if session.activationState == .activated && !session.hasContentPending {
                self.wcBackgroundTasks.forEach { $0.setTaskCompleted() }
                self.wcBackgroundTasks.removeAll()
            }
        }
    }
}

実装の解説は以上になります。

動作結果

watchOS アプリをスリープさせて 2 分後に iOS から watchOS へデータを送信してみたところ、その 3 分後ぐらいに watchOS 側で WKWatchConnectivityRefreshBackgroundTask を取得することができました。

さいごに

本記事では watchOS 3.0 で追加された新クラス WKWatchConnectivityRefreshBackgroundTask の使用方法を紹介しました。

今回は iOS アプリ -> watchOS アプリ方向の User info transfer だけを試しましたが、他の転送方法の場合も同様の手順で試すことができるのではないかと思います。

参考資料