Apple Watch即将于4月下旬发售,而Watch App的开发已成为iOS开发的热点。本文作者通过Watch App的实际开发经验,将其中的一些注意事项总结分享给大家。以下为正文:
接触Apple Watch相关的开发工作已经差不多快三个月时间了,每天都会去逛逛WatchKit苹果的开发者论坛,看看最近都有哪些其他开发者po出来的问题。我自己也遇到不少问题,其中很多都是我自己摸索着解决掉的。
苹果公布的关于Apple Watch的信息很多,用于开发已经足够,但一切感觉都是在抹黑前行,因为无法进行真机测试,包括Handoff,也包括语音输入,以及发布会上的那个类似Emoji 的表情都是些什么。
自己来现在的公司实习到今,主要做的工作几乎都和iOS8新特性有关,毕竟现在公司这个项目实在是太成熟了,摸熟悉也需要一个过程。包括之前的Today Widget,到后来的Handoff,包括因为要适配iPhone6做的适配方面的调研等等,都是从去年WWDC之后的新事物,转眼就到2015年的WWDC了,不知道今年会有哪些革新的新事物。
闲话说到这里吧,是时候总结一下这两个月的收获和掉坑了。
目前开发者网站上的这几部分我觉得是开发Watch 必须学习几遍的东西,还有苹果开发者论坛也是一个不错的交流地方。
在iPhone上,主程序是大哥,其他的小扩展必须让路,但是在Watch上,是不是大哥还要看这个APP主要的功能。如果是一个阅读性质的APP,主程序在手表上作用还真不大,例如阅读新闻等等。如果是这类的应用,想在Watch上出彩,或者让用户使用的次数多一些,就要靠良好的Notification体验,以及极其方便用户生活的Glance了。
如上图,现在手上要做的一个交互是,App启动的时候是六个页面,用户可以左右滑动来切换,这里就需要在MainInterfaceController中使用下边这个方法了。
[WKInterfaceController reloadRootControllersWithNames: _controllersArrays contexts:_contextsArray];
在Watch上页面之间转换传值,很重要的一个纽带就是这个context,传递有用的信息和标识,这个方法中,我传递进入六个controller的interface builder identifier,以及事前拼好的六个context。
因为Watch App 的打开可以是几种不同方式的,可以写一个统一的方法[self showController],在这个方法中去选择启动哪一个具体的Controller。我在.h文件中定义了一个枚举来定义不同的启动方式:
typedef enum { WKOpenForNormal, //普通打开 WKOpenForComment, //打开评论页 WKOpenForFavorite, //打开收藏页 WKOpenForGlance //打开来自glance的内容 } WKOpenType;
因为用户如果选择了点击Glance 来查看具体的内容的话,Glance和MainApp是通过Handoff来实现通信的,我们可以在入口的控制器中的:
- (void)handleUserActivity:(NSDictionary *)userInfo;
这个方法中去将WKOpenType赋值成WKOpenForGlance。
当然了,如果是从Notification来的,我们完全可以通过:
- (void)handleActionWithIdentifier:(NSString *) identifier forRemoteNotification:(NSDictionary *)remoteNotification;
这个方法来根据具体的用户点击的动作来区分不同的打开方式。
这里比较难处理的是,如果用户是从Glance进来的,退出这个控制器,还是要显示那六个页面的,这里我的解决方法是注册通知。在出来的控制器中的- (void)didDeactivate;方法中post出来通知,来让主控制器重新打开六个Page页面。Notification同Glance。
因为程序中多处用到了下边这个方法,因此主程序和Watch App 联合调试就显得非常必要了,在Xcode的一个新beta 的release note中苹果介绍了一种方法。
+ (BOOL)openParentApplication:(NSDictionary *)userInfo reply: (void(^)(NSDictionary *replyInfo, NSError *error)) reply;
完成以上三个步骤,主程序和手表程序上的端点都可以进行调试。
在开发初期,我是在extension中进行数据的申请,这样尝试了一段时间之后发现性能上优化的空间不大,而且写出了很多重复的代码。复用项目中已有的代码是我最好的选择,尤其是一些第三方用pod管理的库,但是考虑到公司的项目已经是非常成熟的了,一些管理的第三方库无法正常的使用,进而又去考虑写一个共用的框架,由于时间问题,项目有点大,抽筋抽骨的不是很合适,所以决定充分发挥openParent这个方法,将申请数据这块放在主程序中,顺便将所有需要“问”主程序的东西全部整理到一个类中,这样就可以充分发挥老代码的作用。
数据策略大致如下:首先为了优化Watch App 的启动速度,采用后台申请数据存起来,Watch每次去使用就可以了,最后处理一下冷启动的问题,这种情况是当安装了我们的软件,没有在iPhone上打开过,直接打开Watch上的程序的时候已然有数据,这么做的话除了第一次会启动的稍微慢一点点之外,剩下的启动速度就会快很多。
具体用到的方法是:
- (void)application:(UIApplication *)application performFetchWithCompletionHandler: (void (^)(UIBackgroundFetchResult result))completionHandler NS_AVAILABLE_IOS(7_0);
我和同事做到这里的时候,就感觉是一个iPhone当做了服务器,而Watch则是一个终端,有什么需要的数据,我们两个人设计好协议,通过openparent这个方法沟通。比如说,软件运行当中如果想要知道一个用户是否登录了,因为没有登录是没有某些功能的,那么这个时候通过openparent咨询一下isLogin就好,判断一下是否登录。
Demo中 watch 端代码实现如下:
[WKInterfaceController openParentApplication:@{@"type":@"isLogin"} reply:^(NSDictionary *replyInfo, NSError *error) {}
Demo中 iphone 服务端代码实现如下:
#pragma mark - WatchKit Data -(void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply { NSString *type = userInfo[@"type"]; NSDictionary *para = userInfo[@"para"]; ... ... NSDictionary *replyInfo; if ([type isEqualToString:@"isLogin"]) { int random = arc4random()%10 + 1; NSString *whetherLogin = @""; if (random == 1) { whetherLogin = @"YES"; }else { whetherLogin =@"NO"; } replyInfo = @{@"whetherLogin":whetherLogin}; } else if ([type isEqualToString:@"isFavorite"]) { ... ... ... reply(replyInfo); }
Demo中有三种协议,分别是是否登录,回复信息,是否收藏,当然都是假的,根据项目需求来进行改变,务必注意的是每一种情况都要回调reply(replyInfo);,否则这个方法实际上会响应失败。
而实际上,项目当中需要在Watch上显示很多图片的,这个就需要异步的申请一下,首要想到的还是SDWebImage这个经典框架,这里就可以在openParent里使用将data请求到,然后返回给Watch。
PS:最后的最后,我们发现使用App Group来通信数据更加的有效率,因此一部分数据的请求采用了App Group来实现。
在SDK发布的初期,我以为新控件之一WKInterfaceGroup可以点击,因为目前来看watch上是没有图层的概念的,复杂的UI布局是相当困难的,布局方式和之前有很大的区别,包括在故事板中的布局方法。当初为了实现产品给过来的UI布局也是脑洞大开啊,比如各种嵌套Group,为了要实现demo中主页的这种感觉,我很自然的想到了,放一个group,背景放图片,其他控件放在group上就好了,解决了无法实现控件在控件之上的问题。但是这就需要group可以点击,盼星星盼月亮之后,Xcode6.2正式版出来之后彻底断了我这个念头,没办法,只能通过另一个控件WKInterfaceTable来实现了,每一页只有一行不就可以了么,只能这么干了。
WKInterfaceTable和UITableView使用上还是有一些不同的,也比UITableView的使用方便了很多。
首先你需要去定义一个Row类,这个Row类相当于一个cell,在这个Row上去布局,如果你的表格中呈现数据的方式不一样,那就要定义不同的Row类。
定义好之后,调用的时候需要使用如下方法:
#pragma mark - UI - (void)setUpUI { [self.newsRowTabel setNumberOfRows:1 withRowType:@"RowForOneNews"]; for (int i = 0; i < self.newsRowTabel.numberOfRows; i++) { JRWKNewsRow *newsRow = [self.newsRowTabel rowControllerAtIndex:i]; [newsRow.newsCategory setText:[NSString stringWithFormat:@"第%ld张",_index+1]]; ... ... } }
RowType唯一标识了一个Row类,这里我设置了只有一行,期间设置Row类中每一个属性的UI数据。
响应点击事件需要去实现:
#pragma mark - Table Row Select -(void)table:(WKInterfaceTable *)table didSelectRowAtIndex:(NSInteger)rowIndex { NSDictionary *contextDic = @{@"PicName":_picName,@"index":[NSNumber numberWithInteger:_index]}; [self presentControllerWithName:WKNEWSDETAILCONTROLLERIDENTIFIER context:contextDic]; }
这里去指定具体要呈现出来的是哪一个Controller。
如果表格中的一行不能点击的话,在故事板中设定的时候把selectable勾选掉就可以了。
API中的几个关于Controller切换的方法当中几乎都有context参数,也就是说传递数据由我们决定了。在十二月份刚开始写程序的时候,我传递的是一个很大的字典,发现在程序启动的时候非常的慢,后来决定写一个模型管理类,controller之间只需要传递一个index就可以了。在demo中保留了完整的类。
HandOff在iOS8之后出现,着实是为了Apple Watch量身打造的好么,实在是太应景了,因此在Watch上合理的运用handoff 是一个顺理成章的事情,而WKInterfaceController也带上了相关的一些方法,实际上是要比iphone上的简单易用一些的。
另一方面,在Glance界面,进入到主App上的时候,handoff也起了决定性的作用,通过handoff将具体的信息交给主App去处理。
主要有两个Api,这个是update了全局的Activity,将我们需要传递的信息打包成一个userinfo即可。
- (void)updateUserActivity:(NSString *)type userInfo: (NSDictionary *)userInfo webpageURL:(NSURL *)webpageURL;
下面这个我还记得是开发者watchkit论坛里有一位开发者问过这个问题,在watchkit里怎么没有干掉Activity这一个方法。后来苹果的工程师估计是采纳了。但实际的效果来看,这个方法作用不大,例如在公司的项目中,几乎每一个页面都是需要handoff的,给它invalidate之后,iphone左下角出现logo就会出现异常甚至是不出现的情况。因此如果不是已经很明确的话,轻易的不要用这个方法。
- (void)invalidateUserActivity;
总之,Handoff是Watch和iPhone沟通的绝佳方式之一,苹果也一直很鼓励使用SDK新出的一些东西来补充自己的App的。不要再幻想(至少是现在)通过Watch上的一个按钮能够使得iPhone 上的Host App 能够打开并且显示在前台了。
(1).dynamic notification中苹果是希望用户在通知中就把所有的信息都看完的,而不希望用户点击内容本身(实际上也是不能点击的)再进入到Watch app 内查看这个通知的内容的,恰恰相反的是,glance 的交互理念是相反的,也就是苹果估计用户点击glance页面本身(实际上是可以点击的)进入到Watch app中进行继续深度阅读的。
(2).关于WKTextInputMode,一开始选择的是WKTextInputModeAllowAnimatedEmoji,后来发现这个是动态的大表情,返回的是这个大表情的data,不太适合我们一一对应到iphone上的emoji表情,于是后来切换到了WKTextInputModeAllowEmoji。而WKTextInputModePlain只是显示了我们所“推荐的”那些回复文本选项。
typedef NS_ENUM(NSInteger, WKTextInputMode) { WKTextInputModePlain, // text (no emoji) from dictation + suggestions WKTextInputModeAllowEmoji, // text plus non-animated emoji from dictation + suggestions WKTextInputModeAllowAnimatedEmoji, // all text, animated emoji (GIF data) };
(3).- (void)becomeCurrentPage; 这个方法主要是在page based页面当中,如果第三页在启动的时候你想让它先出来,就要标识好,在awake里边获取到之后,调用这个方法,注意的是,这个第三页不是立马就出现在手表的表盘之上的,而是从第一页蹦到第二页,然后再第三页这样转的。
(4).推荐一个很好用的工具,叫做Bezel,它能够将模拟器中运行的watch app 映射到真实的手表里,表带的样式也分38mm以及42mm,有很多种,可以更好的查看自己的App在真实手表上的样子。更换表带也很方便,直接拖着下边的某一个样式到Bezel上就自动换了。举个例子,在开发的时候曾想左右留边,但是放在Bezel上就会发现手表自带黑边,于是留下的左右边就是很多余了。
Bezel 下载地址,页面内包含N多种表带
从目前来看,手表上出现push应该是随着手机一起来的,也就是同时去显示在这两个设备上,除非一切外力因素,比如手表关闭了抬手查看通知等。在之前的blog中提到过定义category来区分推送通知,如果没有定义category的故事板的话,就会在手表上显示一个系统默认的简短的通知。上边说道,苹果还是鼓励在notification中将该阅读的内容都阅读完,即使增加按钮也要是一些比较简单的操作,比如说一个日程安排的软件,来了一个push,一个done,一个delete,加上系统的cancel,就可以了。
我尝试了在Dynamic notification中申请了一个图片资源,发现系统就选择去显示Static notification,因此在notification controller内进行的任务的能力有限,这个在开发的时候要慎重。
开发的时候,Xcode自动生成的Payload很重要,可以定义多个payload来进行相应的模拟,搭配不同的category,不同的category故事板。
我依然认为Glance 的地位在Watch上是最重要的,至少在第三方独立app登上Watch前,Glance应该是用户使用最频繁的一个功能。因此Glance上要呈现的东西不能太少,也不能太多,一定要简明扼要,要呈现出最重要的一些东西。例如说如果自己的App不是以天气为主的,放一个天气温度什么的就不是很合适,系统的天气和地图软件还是非常出色的,因此还是在Glance 只体现自己App里边独特的东西最好。
另外,Glance的UI布局是很讲究的,如果可以尽量要按照Xcode 给的Upper和Lower的模板进行UI布局。不能使用任何可以操作的空间,例如按钮这样的,因为Glance就一页(可以滚动也是禁止的),有点像是渲染出来的一张图片似的,因此加个按钮是没有意义的。
同Notification,Glance controller 中进行任务的能力也比较有限,因为众多的Glance会一同呈现出来,用户翻腾着每一个app 的Glance,这就要求用户一扫之后就要呈现出来,一个比较好的解决方法就是Glance要呈现的数据提前的申请好,用的时候拿出来,具体实现的方法也有很多。比如上边提到的App Group。
Glance 以及主App的通信是依靠Handoff来实现的,也就是说用户点击了Glance这个页面之后,进入到主App,要做的事情需要根据传过来的userinfo来决定的,主要就是下边这个方法。
[self updateUserActivity:XXXXX userInfo:userInfo webpageURL:nil];
在入口controller中实现方法,决定启动什么页面,呈现什么内容,可以放在willActivate里边。记住的是请求数据这块一定要放在awake里边,不要放在willActivate里边。
-(void)handleUserActivity:(NSDictionary *)userInfo { wkOpenType = JRWKOpenForGlancedemo; if (userInfo) { NSString *sourceString = [userInfo objectForKey:@"Source"]; NSString *picName = [userInfo objectForKey:@"PicName"]; if ([sourceString isEqualToString:@"Glance"]) { _glancePicName = picName; } } }
根据wkopentype决定启动页面。
switch (wkOpenType) { case JRWKOpenForGlancedemo: //glance page break; case JRWKOpenForNotificationdemo: //notification page break; default: [self showPageBaseddemoController]; //默认启动 break; }
Glance 在demo中的表现形式,demo已经整理好,放在了自己的Github上。
AppleWatchDemo
其实WatchKit的东西真不多,更多的是在一个新的平台遇到的各种问题和bug是最让人头疼的。随着真机的即将到来,开发工作也不再是抹黑前行,这些都是利好的消息。不知道什么时候可以有独立的第三方应用的支持,也不知道WatchKit会丰满到什么程度,总之我个人还是很看好Watch的未来的,毕竟苹果引领的穿戴设备的头。
编后语
为了更好地向读者输出更优质的内容,InfoQ将精选来自国内外的优秀文章,经过整理审校后,发布到网站。本篇文章作者为刘瑞,原文链接。本文已由原作者授权InfoQ中文站转载。
刘瑞,中国科学技术大学苏州研究院在读硕士,喜欢科技产品,也喜欢制作开箱、体验视频。大三起开始自学iOS开发。