Apple Watch アプリから親アプリの情報を更新する。

Apple Watch アプリプログラミング

Apple Watch アプリで使っているデータを親の iPhone アプリに渡して、それを iPhone 側の画面に表示する方法です。


以前にこちら で、Apple Watch 側で動作するアプリを作成しました。

今回はこのアプリのスライダーで操作した値を、親の iPhone アプリにも転送して表示するようにしてみます。Apple Watch から親の iPhone アプリに情報を送って、バックグラウンドで表示内容を更新します。

Xcode 6.2 の WatchKit では Apple Watch から親アプリをフォアグラウンドで立ち上げることはできないらしいです。

親の iPhone アプリに情報を渡すときに使う機能

それでは、まずは親になる iPhone アプリを作成します。

親の iPhone アプリに Apple Watch アプリの情報を渡すためには、WatchKit Extension 内で、WKInterfaceControllerクラス に実装されている次のメソッドを使用します。

class func openParentApplication(_ userInfo: [NSObject : AnyObject]!, reply reply: (([NSObject : AnyObject]!, NSError!) -> Void)!) -> Bool
名称 内容
userInfo: [NSObject : AnyObject]! 親アプリに渡す情報をディクショナリ型で指定します。
reply: (([NSObject : AnyObject]!, NSError!) -> Void)! 親アプリから呼び出してもらうクロージャーです。ここで親アプリからの結果を受け取って WatchKit App 側に反映できます。

このメソッドを WatchKit App Extension 内で実行すると、親の iPhone アプリ側のAppDelegateクラス で次のメソッドが呼び出されます。

func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, reply: (([NSObject : AnyObject]!) -> Void)!)
名称 内容
application: UIApplication! 親アプリの UIApplication インスタンスが渡されます。
userInfo: [NSObject : AnyObject]! WatchKit Extension で[ezclip:symbol-method](openParentApplication:reply:)の呼び出し時に指定した userInfo が渡されます。ディクショナリ型の情報です。
reply: (([NSObject : AnyObject]!) WatchKit Extension で[ezclip:symbol-method](openParentApplication:reply:)の呼び出し時に指定した reply が渡されます。親アプリではこのクロージャーを呼び出す必要があるようです。

この 2 つの機能を使うことで、Apple Watch アプリ側から iPhone アプリ側へ情報を伝えて処理することが可能になります。

Apple Watch アプリから親の iPhone アプリを更新する

それでは上記の機能を利用して、こちら で実装した Apple Watch アプリに、親の iPhone アプリの状態を更新する機能を実装してみます。

機能としては、Apple Watch 側で操作したスライダーの値を親の iPhone アプリに伝えて、親アプリに設置したラベルにその値をバックグラウンドで反映します。

親アプリにラベルを設置する

まず、親の iPhone アプリに表示用のラベルを設置します。

設置方法は普段の iOS アプリの通りです。今回はシングルビューアプリケーションなので、プロジェクト生成時に作成されたMain.storyboard のビューにラベルをひとつ設置しました。

親アプリのコードからラベルにアクセスできるようにする

続いて、さきほど設置したラベルに親アプリのコードからアクセスできるようにアウトレットを用意します。ラベルはビューに配置されているので、それを管理するビューコントローラーに設置するのが妥当でしょう。

今回はViewController.swiftViewControllerクラス 内に定義します。

class ViewController: UIViewController {

	@IBOutlet var countLabel:UILabel!
}

親アプリで、アウトレットとオブジェクトを連結する

設計画面にラベルを配置してコードにアウトレットを定義したら、それらを連結します。

Interface Builder で右側に接続インスペクターを表示して、設計画面からビューコントローラーを選択すると、そこに用意したアウトレットが接続インスペクターに表示されます。

ここのボタン をドラッグして、出てきた線をラベルまで引っ張れば、アウトレットとラベルとが連結されます。

Apple Watch アプリから、親アプリへ情報を送信する

ラベルをコントロールする準備が整ったので、いよいよ親アプリに『Apple Watch 側から情報を受け取って、それを自身のラベルに表示する機能』を実装して行きます。


親アプリへの情報の送信は WatchKit Extension で実装します。

今回は WatchKit Extension で管理しているカウント値が更新されたときに、その内容を親の iPhone アプリにも送信するようにします。

親アプリへアカウント値を送信するメソッドを実装

そのために、まずは親の iPhone アプリへカウント値を送信するためのメソッドをひとつ定義しておきます。

WatchKit Extension では、openParentApplication:reply:メソッド を呼び出すことで任意のディクショナリを親の iPhone アプリ側へ渡せます。

class InterfaceController: WKInterfaceController {

	// 親アプリへカウント値を送信します。
	func updateParentCount() {
		
		let userInfo = ["count" : NSNumber(int: self.count)]
		
		WKInterfaceController.openParentApplication(userInfo) { (reply, error) -> Void in
			
		}
	}
}

今回はディクショナリに "count" をキーにしてカウント値を渡すことにします。このとき NSObject を継承した値を指定する必要があるので、カウント値をNSNumber型 に包んで渡すことにしました。


また、ここの最後に指定したクロージャーを親の iPhone アプリ側から呼び出してもらう約束になっています。

ここには、親の iPhone アプリで用意した任意の情報と、エラー情報とを受け取って処理するクロージャーを指定します。今回はとりあえず、何もしないクロージャーを渡しておくことにしました。

親アプリへアカウント値を送信するメソッドを使用する

さて、今回のアプリではカウント値が更新されたときに変数countdidSetメソッド が呼ばれるように作ってあるので、その中で、上で定義したupdateParentCountメソッド を呼び出します。

class InterfaceController: WKInterfaceController {

	var count:Int32 = 0 {
		
		didSet {
			
			self.countLabel.setText(String(self.count))
			
			// カウント情報が更新されたときに、親アプリにカウントを通知するメソッドを呼び出します。
			self.updateParentCount()
		}
	}
}

これで、親の iPhone アプリへカウント値を送信できるようになりました。

親アプリで、Apple Watch 側から送信された情報を受け取る

親の iPhone アプリが WatchKit Exntension から送信された情報を受け取るには、AppDelegateクラスapplication:handleWatchKitExtensionRequest:reply:メソッド の実装が必要です。

これはWatchKit モジュールで拡張されたメソッドなので、これを利用するために、まずはAppDelegateクラス を実装しているAppDelegate.swift で、WatchKit モジュールをインポートします。

import WatchKit

そして、クラス内にapplication:handleWatchKitExtensionRequest:reply:メソッド を実装します。

class AppDelegate: UIResponder, UIApplicationDelegate {

	// WatchKit Extension から openParentApplication:reply: が実行されると、このメソッドが呼び出されます。
	func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, reply: (([NSObject : AnyObject]!) -> Void)!) {

		// 返信として空の情報を返しておきます。
		reply([:])
	}
}

これで WatchKit Extension から送られてきた情報を、変数userInfo で受け取ることができました。

あとはここで親アプリに対して処理をして、処理結果をここの変数reply で受け取ったクロージャーに渡してあげます。上記の例ではとりあえず、空の情報 [:] を呼び出し元に伝えています。

親アプリで、Apple Watch アプリから受け取った情報をラベルに送る

さて、それでは受け取ったカウント値をラベルに反映させるコードを実装したいところですが、それにはここ (AppDelegate) からビューコントローラーへ情報を伝える方法を考えないといけません。

AppDelegate からビューコントローラーへ情報を伝えるのには、今回は通知センター (NSNotificationCenter) を使うことにします。

AppDelegate の windowプロパティ からビューコントローラーへ辿り着く方法もあるとは思いますが、将来ビューコントローラーをあれこれ切り替え出したときに制御が厄介になりそうなので避けておきます。

通知名を用意する

まず、通知センターで使用する通知名を定義します。

場所はどこでも良いのですが、今回はAppDelegate.swift のクラス定義の外側に、不変値変数で次のように定義しておきます。

/// カウント値が更新されたことを通知します。
let ApplicationUpdateCountNotification = "ApplicationUpdateCountNotification"

WatchKit Extension から情報を受け取ったときに通知を送信する

通知名を準備したら、AppDelegateクラス にさきほど定義したapplication:handleWatchKitExtensionRequest:reply:メソッド 内でカウント値を受け取ったときに通知を送信するコードを追記します。

class AppDelegate: UIResponder, UIApplicationDelegate {

	func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, reply: (([NSObject : AnyObject]!) -> Void)!) {

		// 適切なカウント値が送られてきたときに通知を送信します。
		if let count = userInfo["count"] as? NSNumber {
			
			let nc = NSNotificationCenter.defaultCenter()			
			nc.postNotificationName(ApplicationUpdateCountNotification, object: count)
		}
		
		// 今回は常に空の情報を呼び出し元に返します。
		reply([:])
	}
}

これでカウント値を載せた通知を送信することができました。

ビューコントローラーで通知を受け取る

通知の送信ができたら、ビューコントローラー側でそれを受け取るコードを実装します。実装箇所はViewController.swiftViewControllerクラス 内です。

まずは通知を受け取ったときに呼び出すapplicationUpdateCountNotification:メソッド を実装します。この中で、受け取ったカウント値をラベルに反映します。

class ViewController: UIViewController {

	/// カウント値が通知されたときに、その値をラベルに反映します。
	func applicationUpdateCountNotification(notification:NSNotification) {
		
		let count = notification.object as NSNumber
		
		self.countLabel.text = count.description
	}
}

そして、このメソッドが通知を受け取ったときに呼び出されるようにします。

実装場所は、今回はviewWillAppear:メソッド にしておくことにします。

class ViewController: UIViewController {

	/// ビューの表示直前に通知を受け取れる準備をします。
	override func viewWillAppear(animated: Bool) {

		super.viewWillAppear(animated)
		
		let nc = NSNotificationCenter.defaultCenter()		
		nc.addObserver(self, selector: Selector("applicationUpdateCountNotification:"), name: ApplicationUpdateCountNotification, object: nil)
	}
}

これと対になるように、ビューが非表示にされるときに通知を受け取らないようにしておきます。

class ViewController: UIViewController {

	override func viewWillDisappear(animated: Bool) {

		NSNotificationCenter.defaultCenter().removeObserver(self)
		
		super.viewWillDisappear(animated)
	}
}

これで WatchKit Extension からカウント値を受け取って、ラベルに反映する一連の流れが出来上がりました。

通知を受け取る設定をするタイミングについて補足ですが、WatchKit Extension からopenParentApplication:reply:メソッド が呼び出されたときに親アプリがまだ起動していなければ、親アプリをバックグラウンドで起動してビューが表示されるところまで終わってからapplication:handleWatchKitExtensionRequest:reply:メソッド が呼び出されるので、今回のviewWillAppear:メソッド のタイミングでも WatchKit Extension の情報を取りこぼすことなく受け取れるようでした。

ただし、上記で説明したタイミングだけでは Apple Watch アプリを起動しないまま親アプリを iPhone で直接起動すると、ラベルの値が設定されないので注意してください。iPhone アプリだけで起動した時にラベルを何で初期化するかは WatchKit アプリの話からは外れるので、今回は考えないでおくことにします。

Apple Watch での操作が親アプリに反映されることを確認する

今回のopenParentApplication:reply:メソッド を使った方法では、親アプリが自動ではフォアグラウンドに移行しないため、正しく反映されているかは、iPhone から手動で親アプリを起動する必要があります。

WatchKit App を実行している状態で iPhone シミュレーターからその親アプリを起動すると、ラベルに現在のスライダーの値が表示されているのを確認できます。

そして Apple Watch 側でスライダーを操作すると、それに合わせて親アプリのラベルも更新されます。