Apple Watch用のシンプルなアプリを作成します。 ランブラーの例に関する個人的な経験。

Appleは2015年4月24日に、カリフォルニアでのプレゼンテーションでの最初の発表から6か月後にApple Watchを発表しました。 ランブラーは離れることができませんでした。 WatchKit SDKとガイドラインを確認したところ、現時点では機会がほとんどなく、一般に開発にそれほど時間はかからないことが明らかになりました。



画像画像画像画像







付録ランブラー



Apple Watch用アプリ



監視アプリケーション(WatchKitアプリ)は、電話のアプリケーション(WatchKit拡張機能)と連携して機能します。 計算、ネットワーク要求、およびその他のすべての作業が電話で実行され、結果が時計に送信されます。 WatchKitアプリは、アプリケーションリソース(UI、画像、音声)を格納します。



ドキュメントのリリース前は、WatchKitアプリは独立しており、電話がなくても動作するように思われていました。 現時点では、Appleのいくつかのネイティブアプリケーションを除き、これは不可能です。 WatchKit SDKの将来のバージョンで、スタンドアロンアプリケーションを作成できることを願っています。



画像



時計が電話とペアリングされていない場合、ほとんどのアプリケーションは電話がないことを通知し、ユーザーに少数のオプションが提供されます。

画像



1.時計から音楽を聴く。 どのアプリケーションが音楽を再生および保存できるかは明らかではありません。

画像



2.時間管理、アラーム、タイマー。

画像画像画像



3. Apple PayおよびPassbookへのアクセス。

画像



Appleは、時間を表示したり、他のネイティブ機能を複製したりするアプリケーションを許可していません。 また、アプリケーションのページでPebbleスマートウォッチに言及することはお勧めしません。



ランブラーニュース



Apple Watchで動作するようにアプリケーションを適合させた最初の開発者の波に乗るために、できるだけ早くアプリケーションをリリースしたかったのです。 開発プロセスは難しくありませんでしたが、いくつかの困難に直面しなければなりませんでした。 どうやら、ほとんどの開発者は同じ道をたどっています。 問題は、ドキュメントとガイドラインが単純な質問に対する回答のみを提供することです。 もちろん、開発者の専門フォーラムは、人々が経験や問題を共有する場所を支援します。 運がよければ、WatchKit開発者の1人が答えてくれます。



機能アプリケーションに必要な以下のリストに決めました。







簡単なタスク:ニュース、それらを表示するためのインターフェイスもあります。 問題は、携帯電話から時計にニュースと画像を転送する方法です。 電話で操作が行われることをお知らせします。 WatchKit SDKは、 WKInterfaceController + openParentApplication:reply:のクラスメソッドを提供します。このメソッドは、生成された辞書にWatchKit拡張機能からの最新ニュースを親アプリケーションに送信します。 また、このメソッドは、必要なニュースを送信できる親アプリケーションを呼び出すコールバックを提供します。 親アプリケーションが非アクティブの場合、リクエストはバックグラウンドとして処理され、実行時間は短いと見なされます。 現時点では、電話側でのリクエストの処理に問題があります-iOSはリクエストに時間がかかりすぎていると判断し、途中で終了します。 ここで一時的な解決策が提案されました



アプリグループを使用して電話と時計の間でデータを交換する



要するに、共有読み取り/書き込みリポジトリは、親アプリケーションとWatchKit拡張機能用に提供されます。 この共有リポジトリの使用を検討してください。 リストにアイテムを追加/削除する簡単なアプリケーションの例を考えてみましょう。 コードはgithubで利用できます。ブランチを見てください。それぞれがアプリグループの新しい使用方法に対応しています。



NSUserDefaultsを使用する


NSUserDefaultsは、ごく一部のデータを転送する必要がある場合に適切に機能します。



メインアプリケーションのViewControllerのコード。
- (NSUserDefaults *)defaults { if (_defaults == nil) { // group.com.rambler.demo.shared -  ,   Developer Center _defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.rambler.demo.shared"]; } return _defaults; } ... - (IBAction)add:(id)sender { [self addListItem:[@"Item #" stringByAppendingString:@(self.list.count + 1).stringValue]]; } - (IBAction)remove:(id)sender { [self removeLastListItem]; } ... - (void)addListItem:(id)listItem { [self.list addObject:listItem]; [self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:self.list.count - 1 inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic]; [self.defaults setObject:self.list forKey:@"list"]; [self.defaults synchronize]; } - (void)removeLastListItem { if (self.list.count == 0) { return; } [self.list removeLastObject]; [self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:self.list.count inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic]; [self.defaults setObject:self.list forKey:@"list"]; [self.defaults synchronize]; }
      
      









WatchKit拡張機能のコントローラーコード
 - (NSUserDefaults *)defaults { if (_defaults == nil) { _defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.rambler.demo.shared"]; } return _defaults; } ... - (void)loadList { [self.defaults synchronize]; self.list = [self.defaults objectForKey:@"list"]; [self updateListView]; } - (void)updateListView { if (self.table.numberOfRows) { [self.table removeRowsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.table.numberOfRows)]]; } if (self.list.count > 0) { [self.table insertRowsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.list.count)] withRowType:@"ItemListRowControllerId"]; NSUInteger idx = 0; for (id item in self.list) { ItemListRowController *rowController = [self.table rowControllerAtIndex:idx++]; [rowController.label setText:item]; } } } - (void)willActivate { [super willActivate]; [self loadList]; } - (IBAction)refresh:(id)sender { [self loadList]; }
      
      









データは転送されますが、インターフェースを更新するためにいつ変更されるかを追跡する方法はありません。 したがって、手動で更新する必要があります。



NSUserDefaultsを操作するためのすべてのコードは、 このプロジェクトブランチで利用できます。



NSFileCoordinatorの使用


NSFileCoordinatorは、特に画像を転送する必要がある場合、大量のデータに適しています。 前の例を書き換えて、それを使用してデータが送信されるようにします。



メインアプリケーションのViewControllerのコード。
 - (NSFileCoordinator *)fileCoordinator { if (_fileCoordinator == nil) { _fileCoordinator = [[NSFileCoordinator alloc] init]; } return _fileCoordinator; } - (void)viewDidLoad { [super viewDidLoad]; [self.fileCoordinator coordinateReadingItemAtURL:[self presentedItemURL] options:NSFileCoordinatorReadingWithoutChanges error:nil byAccessor:^(NSURL *newURL) { NSData *data = [NSData dataWithContentsOfURL:newURL]; id object = [NSKeyedUnarchiver unarchiveObjectWithData:data]; self.list = object != nil ? [NSMutableArray arrayWithArray:object] : [@[] mutableCopy]; [self.tableView reloadData]; }]; } ... - (IBAction)add:(id)sender { [self addListItem:[@"Item #" stringByAppendingString:@(self.list.count + 1).stringValue]]; } - (IBAction)remove:(id)sender { [self removeLastListItem]; } #pragma mark Table delegate - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.list.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ListItemCellId"]; cell.textLabel.text = self.list[indexPath.row]; return cell; } #pragma mark List actions - (void)saveListWithCompletion:(void (^)(void))completion { [self.fileCoordinator coordinateWritingItemAtURL:[self presentedItemURL] options:NSFileCoordinatorWritingForReplacing error:nil byAccessor:^(NSURL *newURL) { NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.list]; [data writeToURL:newURL atomically:YES]; if (completion != nil) { completion(); } }]; } - (void)addListItem:(id)listItem { [self.list addObject:listItem]; [self saveListWithCompletion:^{ [self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:self.list.count - 1 inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic]; }]; } - (void)removeLastListItem { if (self.list.count == 0) { return; } [self.list removeLastObject]; [self saveListWithCompletion:^{ [self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:self.list.count inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic]; }]; } #pragma mark NSFilePresenter impl - (NSURL *)presentedItemURL { NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.rambler.demo.shared"]; return [containerURL URLByAppendingPathComponent:@"list"]; } - (NSOperationQueue *)presentedItemOperationQueue { return [NSOperationQueue mainQueue]; }
      
      









WatchKit拡張機能のコントローラーコード
 - (NSFileCoordinator *)fileCoordinator { if (_fileCoordinator == nil) { _fileCoordinator = [[NSFileCoordinator alloc] init]; } return _fileCoordinator; } - (void)awakeWithContext:(id)context { [super awakeWithContext:context]; [NSFileCoordinator addFilePresenter:self]; } - (void)loadList { [self.fileCoordinator coordinateReadingItemAtURL:[self presentedItemURL] options:NSFileCoordinatorReadingWithoutChanges error:nil byAccessor:^(NSURL *newURL) { NSData *data = [NSData dataWithContentsOfURL:newURL]; id object = [NSKeyedUnarchiver unarchiveObjectWithData:data]; self.list = object != nil ? [NSMutableArray arrayWithArray:object] : [@[] mutableCopy]; [self populateListView]; }]; } - (void)populateListView { if (self.table.numberOfRows) { [self.table removeRowsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.table.numberOfRows)]]; } if (self.list.count > 0) { [self.table insertRowsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.list.count)] withRowType:@"ItemListRowControllerId"]; NSUInteger idx = 0; for (id item in self.list) { ItemListRowController *rowController = [self.table rowControllerAtIndex:idx++]; [rowController.label setText:item]; } } } - (void)updateListView:(NSArray *)newList { NSIndexSet *newItemsIndexSet = [newList indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { return ![self.list containsObject:obj]; }]; NSIndexSet *removedItemsIndexSet = [self.list indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { return ![newList containsObject:obj]; }]; [self.table removeRowsAtIndexes:removedItemsIndexSet]; for (id newItem in [newList objectsAtIndexes:newItemsIndexSet]) { [self.table insertRowsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(self.table.numberOfRows, 1)] withRowType:@"ItemListRowControllerId"]; ItemListRowController *rowController = [self.table rowControllerAtIndex:self.table.numberOfRows - 1]; [rowController.label setText:newItem]; } } - (void)willActivate { [super willActivate]; [self loadList]; } - (void)didDeactivate { [super didDeactivate]; } - (IBAction)refresh:(id)sender { [self loadList]; } #pragma mark NSFilePresenter impl - (NSURL *)presentedItemURL { NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.rambler.demo.shared"]; return [containerURL URLByAppendingPathComponent:@"list"]; } - (NSOperationQueue *)presentedItemOperationQueue { return [NSOperationQueue mainQueue]; } - (void)presentedItemDidChange { [self.fileCoordinator coordinateReadingItemAtURL:[self presentedItemURL] options:NSFileCoordinatorReadingWithoutChanges error:nil byAccessor:^(NSURL *newURL) { NSData *data = [NSData dataWithContentsOfURL:newURL]; id object = [NSKeyedUnarchiver unarchiveObjectWithData:data]; NSArray *newItems = [NSMutableArray arrayWithArray:object]; [self updateListView:newItems]; self.list = newItems; }]; }
      
      









完全なコードはこちらから入手できます。



主な改善点は、 NSFilePresenterプロトコルの-(void)presentedItemDidChangeメソッドの使用です。このメソッドは、ファイルが変更されたかどうかを報告します。 これは、WatchKit拡張コードでデータの変更を追跡し、これらの変更に応じてインターフェースを更新できることを意味します。



Mutual Mobileの賢い人々は、NSFileCoordinatorとの連携を大幅に簡素化し、 ダーウィン通知を使用してデータの変更を通知する便利なMMWormholeラッパーを作成しました。 ライブラリはgithubで入手できます。



次の問題:時計から電話のアプリケーションを「起動」することはできません。 選択したニュースで電話でアプリケーションを開く機能を備えたForce Touchメニューを表示したかったのです。 残念ながら、SDKでは現在これを許可していません。 ただし、カメラおよびiMessageアプリケーションは、電話で対応するアプリケーションを起動します。 SDKの新しいバージョンには同様の機能があると思います。 Appleは代わりに、ハンドオフを通信手段として使用することを提案しています。 確かに弱い代替品ですが、機能します。 リリース時までに、アプリケーションがアクティブであるという条件で、電話でニュースを開く機能を追加しました。



電話機と時計の間のデータ交換の未知の速度の不確実性ほど複雑ではありません-BluetoothとWi-Fiが使用されます。 開発の最初に、シミュレータでテストを実施したとき、接続の速度を確立することは不可能でした。 速度が小さいと仮定すると、必要なデータと圧縮画像のみがクロックに送信されます。



後で、テストクロックを操作するときに、UIの速度が低下し、ほとんどすべてのアプリケーションの起動に長時間かかることがわかりました。 遅れの原因は時計のソフトウェアであるとの疑いがあります。 背景画像の送信を無効にしても、アプリケーションの起動速度に大きな影響はありませんでした。 イメージがアプリケーションで再利用される場合-クロックでキャッシュされる必要があります。 確かに、キャッシュはラバーではなく、可能であればクリーニングする必要があります。



一般に、開発は深刻な問題をもたらしませんでした。 主な問題は、テストのためにApple Watchが一時的に存在しないため、シミュレータとドキュメントに依存していたことです。 開発者フォーラムは、Apple Watchをサポートするアプリケーションをホストしようとすると、頻繁にエラーが発生することについても不満を述べました。







このような問題は発生せず、アプリケーションを初めて公開しました。 Apple Watchでアプリケーションをアップロードする人には、 この記事を読むことをお勧めます。



Appleがプラットフォームを開発する方法を探り、開発者に公開できるものを決定しているという印象を受けます。 そして、まもなく(おそらくWWDC'15で)新しいSDKが提供されます。



関連リンク



開発者フォーラム (アカウントが必要)

開発概要

デザイナーとプログラマーのための有用なリソース

WatchKit SDKシリーズブログ



All Articles