Apple Watch をたまたま発売日当日ゲットできたので、いろいろと WatchKit アプリ開発に携わってきた 中で、「実機でやってみないと確信が持てないな。。」と思っていた諸々について検証してみました。
Xcodeからの実機インストール
Parent App をインストールして、Apple Watch アプリからインストールするのか、どうなのか・・・?
というところがよくわかってなかったのですが、やってみると何のことはない、Xcode から WatchKit App の Scheme を選び、Apple Watchとペアリングした iPhone を選択して Runするだけでした。
ちなみに親アプリを実機インストールするだけでもいけました。(ペアリング済みのWatchにアプリが自動的にインストールされる)
あと実機デバッグ時も普通にブレークポイントで止まってくれました。
親アプリからローカル通知を発行するとWatchKit Appで受けられるのか?
ドキュメント読む限りではできそうだけど、シミュレータでは通知受信時の見た目しか確認できないので、実際どうなのか・・・「親アプリとWatchアプリのどちらが通知を受け取るか」はiOSが制御する、と書いてあるし、親アプリが自分で受け取ってしまうということもありうるのか・・・!?
というわけで、次のように親アプリ側で「ボタンを押したら10秒後にローカル通知を発行する」という実装をしておいて、
UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
- (IBAction)notifyBtnTapped:(id)sender { UILocalNotification *localNotification = [UILocalNotification new]; localNotification.alertBody = @"Hello!"; localNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:10.]; localNotification.soundName = UILocalNotificationDefaultSoundName; [[UIApplication sharedApplication] scheduleLocalNotification:localNotification]; }
Apple Watch とペアリングした状態で試してみました。
ケース1: 親アプリがフォアグラウンド状態にある
通知発行ボタンを押して、そのまま親アプリが起動した状態にしておいた場合
→ Apple Watch側では通知を受け取らず
ケース2: 親アプリがバックグラウンド、iPhoneはロックされていない状態
通知ボタンを押して、ホームボタンで親アプリをバックグラウンド状態にし、iPhoneはロックせずにそのままにしておいた場合
→ 親アプリでは通知バナーが表示されたが、Apple Watch側では通知を受け取らず
ケース3: iPhone側がロック状態にある
通知ボタンを押して、電源ボタンでiPhoneをロック状態にした場合
→ 親アプリ、Apple Watch両方で通知を受け取った
というわけで、iPhone側ですぐに反応できない場合にウォッチ側で通知を受け取って表示する、という制御のようです。
Apple Watch 自体の BLE 仕様
Apple Watch の BLE 機能をデベロッパが制御できないのは WatchKit の API を見ればわかることですが、BLE自体は積んでるはず。アドバタイズメントパケット飛んでるのかな・・・?と見てみたところ・・・
普通にありました。
さすがに接続は拒否されるだろう、と思いつつダメ元で繋いでみたら・・・
何の問題もなく繋げました。
(繋いでみてわかる範囲で)Apple Watch の BLE 仕様をまとめてみるとこんな感じ。
- アドバタイズメントパケット
- Connectable
- サービスUUIDのアドバタイズはなし
- サービス
- UUID: D0611xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- キャラクタリスティックを1つだけ保持(下記)
- キャラクタリスティック
- UUID: 8667xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- Properties: Write, Notify
Notification の Subscribe(Light Blue のUIだと Listen)も普通にできました。
念のため参考図書です↓↓
ソシム
売り上げランキング: 898
【電子版】(PDF・達人出版会)
デジタルクラウンでスクロール
「アップルApple Watchのアプリ開発で、できないことのまとめ」という記事に、
開発者向けツールのWatchKitでは、タッチスクリーン上で指を使ったスクロールを可能にするAPIが提供されておらず、デジタルクラウンも使えない仕様となっています。
という記述があり、「明示的にAPIはなくても、普通に WKInterfaceTable とかのスクロールはデジタルクラウンでできるのでは?」と思ってましたが、どうなのでしょうか・・・?
→ WKInterfaceTable はデジタルクラウンでスクロールできました。
フォアグラウンドにある親アプリの制御
前述のWatchKitでできないことのまとめ記事に、こんな記述もありました。
- 離れたiPhoneのカメラへのアクセスはできない
ただし、アップルは自社のカメラアプリからの利用は可能にするようです。
これも、カメラ機能を持った親アプリをフォアグラウンドにしておけば、普通に `openParentApplication:reply:` でシャッター切れるんじゃないか、と思ったので、試してみました。
親アプリ側で下記のように実装をしておいて、
(ViewController)
<UIImagePickerControllerDelegate> @property (nonatomic, strong) UIImagePickerController *pickerCtr;
self.pickerCtr = [[UIImagePickerController alloc] init]; self.pickerCtr.sourceType = UIImagePickerControllerSourceTypeCamera; [self presentViewController:self.pickerCtr animated:YES completion:nil];
- (void)takePicture { [self.pickerCtr takePicture]; }
(AppDelegate)
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply { CameraViewController *vc = (CameraViewController *)self.window.rootViewController; [vc takePicture]; }
WatchKit Extension 側では下記のように実装をしました。
- (IBAction)takePicture:(id)sender { [WKInterfaceController openParentApplication:@{@"command": @"take_picture"} reply:^(NSDictionary *replyInfo, NSError *error) { }]; }
で、親アプリを起動しておいて、WathKit App側のボタンを押すと・・・
→ 無事シャッターが切れました
結論:WatchKit App からフォアグラウンドにある親アプリのカメラ機能等を制御することは可能*1
iPhone側のBLE機能を利用
ウォッチ側のBLE機能はデベロッパは「直接的には」利用できないことは上に書いた通りですが、次の2通りの方法でiPhone側のBLE機能を利用することが考えられます。
- WatchKit Extension で Core Bluetooth を利用
- 親アプリで Core Bluetooth を利用
自分が実装したとある案件では親アプリが既にBLE利用機能を有していたので後者を選択しました。
ここではシンプルな例として、「ウォッチ側のボタンを押したら、バックグラウンドにいる親アプリが外部デバイス(ペリフェラル)との接続を確立する」ということを考えてみます。
バックグラウンドにおけるBLEのふるまいや制約については拙著に詳しく書いたのでここでは割愛するとして、WatchKit App をトリガとして Parent App にBLE関連処理を行わせる際のポイントとなるのは、BLEはスキャン、接続、サービス/キャラクタリスティック探索、Read/Write 等々、基本的には非同期的にレスポンスが返ってくる処理ばかりである、というところです。
`openParentApplication:reply:` は同期処理だし、非同期処理完了後にバックグラウンドの親アプリ側から WatchKit App を起こしたりデータを渡したりするAPIはありません。プッシュ通知やローカル通知を使う方法はありますが、プッシュ通知はタイムラグもありますし、ローカル通知は上で行った検証の通りウォッチ側で受け取れないケースが多すぎます。
で、僕はWatchKit Appからポーリングする実装にしました。
(ウォッチ側)
- (IBAction)connectBtnTapped:(WKInterfaceButton *)sender { // 接続処理開始をparentに依頼する [WKInterfaceController openParentApplication:@{@"command": @"connect"} reply: ^(NSDictionary *replyInfo, NSError *error) { if (error) { return; } [self.connectBtn setEnabled:NO]; [self.connectBtn setTitle:@"Connecting..."]; startTime = [[NSDate date] timeIntervalSince1970]; self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(handleTimer:) userInfo:nil repeats:YES]; }]; } - (void)handleTimer:(NSTimer *)timer { NSTimeInterval interval = [[NSDate date] timeIntervalSince1970] - startTime; if (interval >= kTimeoutInterval) { // タイムアウト [self invalidateTimerIfNeeded]; [self resetInterfaces]; } // 接続状態をparentに確認する [WKInterfaceController openParentApplication:@{@"command": @"status"} reply: ^(NSDictionary *replyInfo, NSError *error) { if (error) { return; } BOOL isReady = [replyInfo[@"is_ready"] boolValue]; if (isReady) { // 次の処理へ } }]; }
(親アプリ側)
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply { NSString *command = userInfo[@"command"]; NSDictionary *replyDic; // connection 開始 if ([command isEqualToString:@"connect"]) { replyDic = [self startConnecting]; } // BLE接続ステータスを返す else if([command isEqualToString:@"status"]) { BOOL isReady = [[BLEManager sharedManager] isReadyForControl]; replyDic = @{@"is_ready": @(isReady)}; } // 中略 reply(replyDic); }
AppleWatchは実機がないのでシミュレータでしか試せない、BLE機能はシミュレータでは使えないというジレンマで検証できずにいたわけですが、今日手に入ったのでさっそく試してみたところ・・・
→ 無事動作しました
しつこいようですが参考図書になります。。m(__)m
↓↓↓↓
ソシム
売り上げランキング: 898
【電子版】(PDF・達人出版会)
*1:PowerPointを制御できるアプリとかもあるので、予想はついていたことですが。。