Xcodeをチマチマと。

Xcodeビギナーによるブログです。

iPhoneとAppleWatchの値渡し

目標:iPhoneで選択した値をAppleWatchに表示する、および、AppleWatchで選択した値をiPhoneに表示する。(※ Simulator)

通信できいない状態のときに変更されるかも知れないので、タイムスタンプ(String)を用いて最新の選択を表示できるようにもする。通信の種類が不適切なものもあるが、仕様をシンプルにするため、1種類のみにした。

  1. AppleWatch Simulator(以下AppleWatch)をiPhone Simulator(以下iPhone)をペアリング
  2. iPhoneとAppleWatchの値渡しの概要
  3. iPhone側のStoryBoardとViewControllerの修正
  4. AppleWatch側のInterfaceとInterfaceControllerの修正

まずは、AppleWatchアプリ作成のプロジェクトを新規作成する。

f:id:JoeCola:20180805114714p:plain

 1. AppleWatch Simulator(以下Watch)をiPhone Simulator(以下iPhone)をペアリング

XCodeの Window > Devices and Simulators で右ペインの PAIRED WATCHES で+ボタンを押してペアリングさせる。

f:id:JoeCola:20180805115445p:plain

 2. iPhoneとAppleWatchの値渡しの概要

値を送りたいタイミングで、次図の上にあるメソッドを使用する。

すぐに値を送るものと、機を見ていいタイミングで値を送るものとがある。

参照頁。このページは大変良い。

それで、次図の下にあるメソッドを構えておくと、値を受け取ることができる。

逆の送受信も同じ。

f:id:JoeCola:20180805140245p:plain

 3. iPhone側のMainStoryBoardとViewControllerの修正

①MainStoryBoardの修正

次のように設定。

・TextField配置。ViewControllerにIBOutletを引っ張る。

・ButtonのA〜Cを配置。すべてのButtonをVirewControllerの同じActionに引っ張る。

f:id:JoeCola:20180805151709p:plain

② ViewControllerの修正

とりあえず全文。

import UIKit

import WatchConnectivity // (A)

 

class ViewController: UIViewController ,WCSessionDelegate { // (B)

    var context: [String: Any]? = nil // (C)

    @IBOutlet weak var field: UITextField!

    @IBAction func touchButton(_ sender: UIButton) {

        let ABC = sender.titleLabel?.text // (D)

        let newContext = ["TIMESTAMP": TimeStamp, "ABC": ABC!]

        context = newContext

        UserDefaults.standard.set(newContext, forKey: "CONTEXT") // (E)

        field.text = ABC!

        if WCSession.isSupported(){

            try? WCSession.default.updateApplicationContext(newContext) // (F)

        }

    }

    override func viewDidLoad() {

        super.viewDidLoad()

        let defaults = UserDefaults.standard

        let defaultContext = defaults.dictionary(forKey: "CONTEXT")

        if defaultContext == nil {// (G)

            let newContext = ["TIMESTAMP": TimeStamp, "ABC": "A"]

            defaults.set(newContext, forKey: "CONTEXT")

        }

        if context == nil{// (H)

            self.setContext(UserDefaults.standard.value(forKey: "CONTEXT") as! [String : Any])

        }

        if WCSession.isSupported(){// (I)

            let session = WCSession.default

            session.delegate = self

            session.activate()

        }

    }

    override func didReceiveMemoryWarning() {

        super.didReceiveMemoryWarning()

    }

    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { // (J)

        print("ViewController_SessionactivationDidCompleteWith:")

        print(activationState)

    }

    func sessionDidBecomeInactive(_ session: WCSession) { // (K)

        print("ViewController_SessionDidBecomeInactive:")

    }

    func sessionDidDeactivate(_ session: WCSession) { // (L)

        print("ViewController_SessionDidDeactivate:")

    }

    func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {

        if context == nil { // (M)

            self.setContext(applicationContext)

        }else {

            let timeStamp = context!["TIMESTAMP"] as! String

            let applicationTimeStamp = applicationContext["TIMESTAMP"] as! String

            if timeStamp < applicationTimeStamp {

                self.setContext(applicationContext)

            }else if timeStamp > applicationTimeStamp {

                try? session.updateApplicationContext(context!)

            }

        }

    }

    func setContext(_ context:[String: Any]){ // (N)

        self.context = context

        let ABC = context["ABC"] as! String

        DispatchQueue.main.async { // (O)

            self.field.text = ABC

        }

        UserDefaults.standard.set(context, forKey: "CONTEXT")

    }

    var TimeStamp: String { // (P)

        let date = Date()

        let formatter = DateFormatter()

        formatter.dateFormat = "yyyyMMddHHmmss"

        return formatter.string(from: date)

    }

}

(A) iPhoneとの通信のためのフレームワーク

(B) WCSessionクラスのデリゲートを設定。

(C) データを保持するためのプロパティ。

(D) ボタンがタップされたとき、ボタンのタイトルを取得し分岐させる。

(E) データの永続保持のためにUserDefaultsを使用。

(F) AppleWatchに送信。

(G) UserDefaultsにデータがなかったときに初期化。

(H) プロパティに値がセットされていないとき、UserDefaultsから値取得。

(I) iPhoneとの通信がサポートされているかどうか確認し、サポートされていれば、デリゲートをセットし、通信をアクティブにする。

(J)〜(L) WCSessionDelegateの必須メソッド。

(M) WatchからのupdateApplicationContext( )を受け取るメソッド。送られてきたcontextのTimestampを調べ、Watch側の方が新しければ更新、iPhone側が新しければ送り返すように設定。

(N) contextを元に、プロパティとラベルに値を流し込むメソッド。

(O) UIViewの書き換えはメインスレッドでしかできない。衝突が起こらないように注意して使用する。

(P)  現在時刻からタイムスタンプ文字列を返す。

 

 4. AppleWatch側のInterfaceStoryBoardとInterfaceControllerの修正

① RowControllerファイルの作成

UIKitで言うところのUITableViewCellのようなファイル。NSObjectのサブクラス。

File > New > File > WatchKit Class

クラス名はTableRowController、ターゲットがWatchKitExtensionになっていることを確認し作成する。

② InterfaceStoryBoardの修正

次のように設定。

・「TEST_WATCH_2」はLabel。InterfaceControllerにIBOutletを引っ張る。

・「Label」はTableをまず置いて、その上に置いたLabel。TableRowControllerにIBOutletを引っ張る。

・TableのRowControllerのクラスをTableRowControllerにセットし、IdentifierをTableRowにセット。

f:id:JoeCola:20180805141450p:plain

② InterfaceControllerの修正

 とりあえず、全文。

 

import WatchKit

import Foundation

import WatchConnectivity // (A)

 

class InterfaceController: WKInterfaceController, WCSessionDelegate { // (B)

    @IBOutlet var table: WKInterfaceTable!

    @IBOutlet var field: WKInterfaceLabel!

    var items = ["A","B","C"] // (C)

    var context: [String: Any]? = nil // (D)

    override func awake(withContext context: Any?) {

        super.awake(withContext: context)

        table.setNumberOfRows(items.count, withRowType: "TableRow") //(E)

        for (index, item) in items.enumerated(){

            let controller = table.rowController(at: index) as! TableRowController

            controller.label.setText(item)

        }

        if WCSession.isSupported(){ // (F)

            let session = WCSession.default

            session.delegate = self

            session.activate()

        }

    }

    override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) { // (G)

        let newContext = ["TIMESTAMP": TimeStamp, "ABC": items[rowIndex]]

        self.setContext(newContext)

        if WCSession.isSupported(){

            try? WCSession.default.updateApplicationContext(newContext)

        }

    }

    override func willActivate() {

        super.willActivate()

        if context == nil {

            if WCSession.isSupported(){

                let dammyContext = ["TIMESTAMP": "0", "ABC": ""] // (H)

                try? WCSession.default.updateApplicationContext(dammyContext)

            }else {

                let newContext = ["TIMESTAMP": TimeStamp, "ABC": "A"]

                setContext(newContext)

            }

        }

    }

    override func didDeactivate() {

        super.didDeactivate()

    }

    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { // (I)

        print("InterfaceController_activationDidCompleteWith:")

        print(activationState)

    }

    func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { // (J)

        if context == nil {

            self.setContext(applicationContext)

        }else {

            let timeStamp = context!["TIMESTAMP"] as! String

            let applicationTimeStamp = applicationContext["TIMESTAMP"] as! String

            if timeStamp < applicationTimeStamp {

                self.setContext(applicationContext)

            }else if timeStamp > applicationTimeStamp {

                try? session.updateApplicationContext(context!)

            }

        }

    }

    func setContext(_ context:[String: Any]){ // (K)

        self.context = context

        let ABC = context["ABC"] as! String

        field.setText(ABC)

    }

    var TimeStamp: String { // (L)

        let date = Date()

        let formatter = DateFormatter()

        formatter.dateFormat = "yyyyMMddHHmmss"

        return formatter.string(from: date)

    }

}

(A) iPhoneとの通信のためのフレームワーク

(B) WCSessionクラスのデリゲートを設定。

(C) 選択肢配列。

(D) データを保持するためのプロパティ。

(E) 選択肢を表示するTableの初期化。UIKitとは様相が異なるので注意。

(F) iPhoneとの通信がサポートされているかどうか確認し、サポートされていれば、デリゲートをセットし、通信をアクティブにする。

(G) Tableを選択した時に呼ばれる。デリゲートメソッドではない。新しいcontextを作成し、setContextする。

(H) アクティブになったとき、contextが空なら、iPhoneに問い合わせ。通信がすぐにできないときは、通信ができたときに値を取得する仕様。

(I) WCSessionDelegateの必須メソッド。

(J)iPhoneからのupdateApplicationContext( )を受け取るメソッド。送られてきたcontextのTimestampを調べ、iPhone側の方が新しければ更新、Watch側が新しければ送り返すように設定。

(K) contextを元に、プロパティとラベルに値を流し込むメソッド。

(L) 現在時刻からタイムスタンプ文字列を返す。

 

以上で、Runすると次のような画面が出て、相互のアクションを送り合います。

※諸事情により、まだ実機では試していません。

f:id:JoeCola:20180809103422p:plain