📜 ⬆️ ⬇️

Create a simple app for the Apple Watch. Personal experience on the example of Rambler. News

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.

imageimageimageimage


')
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.

image

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:
image

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

2. Time management, alarm clocks, timers.
imageimageimage

3. Access Apple Pay and Passbook.
image

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 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:



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 Notes
Useful resources for designers and programmers
WatchKit SDK article series blog

Source: https://habr.com/ru/post/257315/


All Articles