Apple Watch两个月开发的一些收获总结

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 必须学习几遍的东西,还有苹果开发者论坛也是一个不错的交流地方。

  • WatchKit Framework Reference
  • WatchKit Development Tips : Optimize your WatchKit apps with these tips and best practices.
  • Apple Watch Programming Guide
  • Developer Forum

1. Watch Main App

在iPhone上,主程序是大哥,其他的小扩展必须让路,但是在Watch上,是不是大哥还要看这个APP主要的功能。如果是一个阅读性质的APP,主程序在手表上作用还真不大,例如阅读新闻等等。如果是这类的应用,想在Watch上出彩,或者让用户使用的次数多一些,就要靠良好的Notification体验,以及极其方便用户生活的Glance了。

(1)以Page-Based方式启动Watch App

Apple Watch两个月开发的一些收获总结_第1张图片

如上图,现在手上要做的一个交互是,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。

(2)Watch App 与Host App 联合调试

因为程序中多处用到了下边这个方法,因此主程序和Watch App 联合调试就显得非常必要了,在Xcode的一个新beta 的release note中苹果介绍了一种方法。

+ (BOOL)openParentApplication:(NSDictionary *)userInfo reply:
(void(^)(NSDictionary *replyInfo, NSError *error)) reply;
  1. 首先run 起来Apple Watch App在模拟器中。
  2. 在iphone 模拟器中启动 demo App。
  3. Xcode - Debug - Attach to Process 里找到host app 线程,Attach上。

完成以上三个步骤,主程序和手表程序上的端点都可以进行调试。

(3)申请数据方面

在开发初期,我是在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来实现。

(4)TableView 在Watch 上的使用

在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勾选掉就可以了。

Apple Watch两个月开发的一些收获总结_第2张图片

(5)数据在Controller 间的传递

API中的几个关于Controller切换的方法当中几乎都有context参数,也就是说传递数据由我们决定了。在十二月份刚开始写程序的时候,我传递的是一个很大的字典,发现在程序启动的时候非常的慢,后来决定写一个模型管理类,controller之间只需要传递一个index就可以了。在demo中保留了完整的类。

(6)关于HandOff

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 能够打开并且显示在前台了。

(7)其他一些Tips

(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多种表带

Apple Watch两个月开发的一些收获总结_第3张图片

2.Notification

从目前来看,手表上出现push应该是随着手机一起来的,也就是同时去显示在这两个设备上,除非一切外力因素,比如手表关闭了抬手查看通知等。在之前的blog中提到过定义category来区分推送通知,如果没有定义category的故事板的话,就会在手表上显示一个系统默认的简短的通知。上边说道,苹果还是鼓励在notification中将该阅读的内容都阅读完,即使增加按钮也要是一些比较简单的操作,比如说一个日程安排的软件,来了一个push,一个done,一个delete,加上系统的cancel,就可以了。

我尝试了在Dynamic notification中申请了一个图片资源,发现系统就选择去显示Static notification,因此在notification controller内进行的任务的能力有限,这个在开发的时候要慎重。

开发的时候,Xcode自动生成的Payload很重要,可以定义多个payload来进行相应的模拟,搭配不同的category,不同的category故事板。

3.Glance

我依然认为Glance 的地位在Watch上是最重要的,至少在第三方独立app登上Watch前,Glance应该是用户使用最频繁的一个功能。因此Glance上要呈现的东西不能太少,也不能太多,一定要简明扼要,要呈现出最重要的一些东西。例如说如果自己的App不是以天气为主的,放一个天气温度什么的就不是很合适,系统的天气和地图软件还是非常出色的,因此还是在Glance 只体现自己App里边独特的东西最好。

另外,Glance的UI布局是很讲究的,如果可以尽量要按照Xcode 给的Upper和Lower的模板进行UI布局。不能使用任何可以操作的空间,例如按钮这样的,因为Glance就一页(可以滚动也是禁止的),有点像是渲染出来的一张图片似的,因此加个按钮是没有意义的。

Apple Watch两个月开发的一些收获总结_第4张图片

同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

Apple Watch两个月开发的一些收获总结_第5张图片

4.总结

其实WatchKit的东西真不多,更多的是在一个新的平台遇到的各种问题和bug是最让人头疼的。随着真机的即将到来,开发工作也不再是抹黑前行,这些都是利好的消息。不知道什么时候可以有独立的第三方应用的支持,也不知道WatchKit会丰满到什么程度,总之我个人还是很看好Watch的未来的,毕竟苹果引领的穿戴设备的头。

编后语

为了更好地向读者输出更优质的内容,InfoQ将精选来自国内外的优秀文章,经过整理审校后,发布到网站。本篇文章作者为刘瑞,原文链接。本文已由原作者授权InfoQ中文站转载。

作者信息

刘瑞,中国科学技术大学苏州研究院在读硕士,喜欢科技产品,也喜欢制作开箱、体验视频。大三起开始自学iOS开发。

你可能感兴趣的:(Apple Watch两个月开发的一些收获总结)