On April 24, 2015, Apple released the Apple Watch smart watch, six months after their first announcement at a presentation in California. Rambler could not stay away. After reviewing the WatchKit SDK and
guidelines, it became clear to us that at the moment there are few opportunities and in general, the development should not take a lot of time.




')
Appendix
Rambler .
News .
Apple Watch Apps
The watch application (WatchKit app) works in tandem with the application on the phone (WatchKit extension). Calculations, requests to the network and all other work are performed on the phone and the results are transmitted to the clock. WatchKit app, in turn, stores application resources - UI, images, sounds.
Before the release of the documentation, it seemed that the WatchKit app would be independent and could work without the help of a phone. At the moment, there is no such possibility, not counting several native Apple applications. Hopefully, later versions of WatchKit SDK will allow you to create standalone applications.

If the clock is not paired with the phone, most applications signal the absence of the phone and the user is provided with a small number of possibilities:

1. Listening to music, from hours. It is not clear which applications will be able to play and store music.

2. Time management, alarm clocks, timers.



3. Access Apple Pay and Passbook.

Apple does not allow making applications that show time or duplicate any other native functionality. Also, it is not recommended to mention the
Pebble smart watch on the page of your application.
Rambler. News
We wanted to release the application as quickly as possible to get into the first wave of developers who have adapted their applications to work with the Apple Watch. The development process turned out to be simple, but had to face a couple of difficulties. Apparently, most developers have gone the same way. The problem is that the documentation + guidelines give answers only to simple questions. Of course, the profile development forum helps, where people share their experiences and problems. If you're lucky, one of the WatchKit developers can answer you.
We stopped at the following list, required from the application of functionality:
- The application on the clock receives news headlines and displays them in a page-by-page view.
- For the news image is loaded, which is used as a background.
- Action by clicking on the news - opening an article on the phone
The task is simple: there is news, an interface for displaying them too. The problem is how to transfer the news and image from the phone to the clock. I remind you that the operations take place on the phone. WatchKit SDK provides a class method
WKInterfaceController + openParentApplication: reply:, which sends a generated dictionary from WatchKit extension to the parent application with a request for fresh news. Also, this method provides a callback that will call the parent application where you can send the news we need. If the parent application is inactive, then the request is processed as background and the execution time is assumed to be short. At the moment there is a problem with processing the request on the phone side - iOS considers that the request lasts too long and completes it prematurely. Temporary solution offered
here .
Data exchange between phone and clock using App Groups
In short, it provides a shared read / write repository for the parent application and the WatchKit extension. Consider using this shared repository. Consider the example of a simple application that adds-removes elements from the list. The code is available on
github , look at the branches - each corresponds to a new way of using App Groups.
Using NSUserDefaults
NSUserDefaults work well when you need to transfer a small portion of data.
Code in ViewController main application.- (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]; }
Controller code for WatchKit extension - (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]; }
Data is transferred, but it is not possible to track when it changes to update the interface. Therefore, you have to update it manually.
All code for working with NSUserDefaults is available in
this branch of the project.
Using NSFileCoordinator
For large amounts of data, the NSFileCoordinator is much better suited, especially if you need to transfer images. We will rewrite the previous example so that the data is transmitted with its help.
Code in ViewController main application. - (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]; }
Controller code for WatchKit extension - (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; }]; }
Full code is available
here.The main improvement is the use of the method
- (void) presentedItemDidChange from the
NSFilePresenter protocol, which reports if the file has changed. This means that we can track changes in the data in the WatchKit extension code and update the interface in accordance with these changes.
Smart people from Mutual Mobile have written a convenient MMWormhole wrapper, which greatly simplifies working with NSFileCoordinator and uses
Darwin Notifications to notify you when data changes. The library is available on
github .
The next difficulty: you can not “wake up” the application on the phone from your watch. We wanted the Force Touch to display a menu with the ability to open the application on the phone with the selected news. Unfortunately, now the SDK does not allow this. However, the Camera and iMessage applications launch the corresponding applications on the phone. I think the new version of the SDK will have similar functionality. Apple instead offers to use Handoff as a means of communication. Weak, I confess, replacement, but it works. By the time of release, we added the ability to open news on the phone, provided that the application is active.
Not so much complexity, how much uncertainty was in the unknown speed of data exchange between the phone and the clock - using Bluetooth and Wi-Fi. At the beginning of development, when testing was performed on the simulator, it was impossible to establish how fast the connection would be. Assuming that the speed will be low, only the necessary data and compressed images are transmitted to the clock.
Later, when working with a test clock, it was noticeable that the UI slows down, and the launch of almost all applications lasts a long time. There is a suspicion that the cause of the lags is the clock software, The off background image transfer did not greatly affect the launch speed of our application. If the image will be reused in the application - it should be cached on the clock. True, the cache is not rubber, and if possible it should be cleaned.
In general, the development did not bring serious problems. The main difficulty is the temporary absence of Apple Watch for testing, so they relied on the simulator and documentation. The developers forum also complained about frequent failures when trying to host applications with Apple Watch support, and the reasons were different:
- The application does not follow the guidelines.
- The screenshots of the Apple Watch application are made in the frame of the images of the watch themselves, for example using Bezel
- Using private API
- Application description in the App Store does not follow the instructions of Apple
We have not encountered such problems and published the application from the first time. For those who are going to lay out applications with Apple Watch, I recommend reading
this article
It seems that Apple probes the development paths of the platform, determining what can be opened to developers. And soon (presumably at WWDC'15) we will be presented with a new SDK.
Related Links
Developer Forum (account required)
Brief Development NotesUseful resources for designers and programmersWatchKit SDK article series blog