随着今天凌晨 Apple 发布了第一版的 Watch Kit 的API,对于开发者来说,这款新设备的一些更详细的信息也算是逐渐浮出水面。可以说第一版的 WatchKit开放的功能总体还是令人满意的。Apple 在承诺逐渐开放的方向上继续前进。本来在 WWDC 之后预期 Today Widget会是各类新颖 app 的舞台以及对 iOS 功能的极大扩展,但是随着像 Launcher 和 PCalc 这些创意型的 TodayWidget 接连被下架事件,让开发者也不得不下调对 WatchKit 的预期。但是至少从现在的资料来看,WatchKit是允许进行复杂交互以及完成一些独立功能的。虽然需要依托于 iPhone app,但是至少能够发挥的舞台和空间要比我原先想象的大不少。
当然,因为设备本身的无论是电量还是运算能力的限制,在进行 Watch app 开发的时候也还是有很多掣肘。现在 Watchapp 仅仅只是作为视图显示和回传用户交互的存在,但是考虑到这是这款设备的第一版 SDK,另外 Apple也有承诺之后会允许真正运行在 Watch 上的 app 的出现,Apple Watch 和 WatchKit的未来还是很值得期待的。废话不再多,我们来简单看看 WatchKit 的一些基本信息吧。
我们能做什么
Watch app 架构
首先需要明确的是,在 iOS 系统上,app 本体是核心。所有的运行实体都是依托在本体上的:在 iOS 8之前这是毋庸置疑的,而在 iOS 8 中添加的各种 Extension 也必须随¥同 app 本体捆绑,作为 app的功能的补充。Watch app 虽然也类似于此,我们要针对 Apple Watch 进行开发,首先还是需要建立一个传统的 iOSapp,然后在其中添加 Watch App 的 target。在添加之后,会发现项目中多出两个 target:其中一个是WatchKit 的扩展,另一个是 Watch App。在项目相应的 group 下可以看到,WatchKit Extension中含有代码 (InterfaceController.h/m等),而Watch App里只包含了Interface.storyboard。不过暂时看来的好消息是Apple 并没有像对 iPhone Extension 那样明确要求针对 Watch 开发的 app 必须还是以 iOS app为核心。也就是说,将 iOS app 空壳化而专注提供 Watch 的 UI和体验也许是被允许的。
在应用安装时,负责逻辑部分的 WatchKit Extension 将随 iOS app 的主 target 被一同安装到iPhone 中,而负责界面部分的 WatchKit App 将会在主程序安装后由 iPhone 检测有没有配对的 AppleWatch,并提示安装到 Apple Watch 中。所以在实际使用时,所有的运算、逻辑以及控制实际上都是在 iPhone中完成的。在需要界面刷新时,由 iPhone 向 Watch发送指令进行描画并在手表盘面上显示。反过来,用户触摸手表交互时的信息也由手表传回给 iPhone 并进行处理。而这个过程WatchKit 会在幕后为我们完成,并不需要开发者操心。我们需要知道的就是,原则上来说,我们应该将界面相关的内容放在 WatchApp 的 target 中,而将所有代码逻辑等放到 Extension 里。在手表上点击 app 图标运行 Watch App 时,手表将会负责唤醒手机上的 WatchKit Extension。而WatchKit Extension 和 iOS app 之间的数据交互需求则由 App Groups 来完成,这和 TodayWidget 以及其他一些 Extension是一样的。如果你还没有了解过相关内容,可以参看我之前写过的一篇TodayExtension 的教程。
主要类
WKInterfaceController 和生命周期
WKInterfaceController是 WatchKit中的UIViewController一样的存在,也会是开发Watch App时花时间最多的类。每个WKInterfaceController或者其子类应该对应手表上的一个整屏内容。但是需要记住整个WatchKit 是独立于 UIKit而存在的,WKInterfaceController是一个直接继承自NSObject的类,并没有像UIKit中UIResponser那样的对用户交互的响应功能和完备的回调。
不仅在功能上相对UIViewController简单很多,在生命周期上也进行了大幅简化。每个WKInterfaceController对象必然会被调用的生命周期方法有三个,分别是该对象被初始化时的-initWithContext:,将要呈现时的-willActivate以及呈现结束后的-didDeactivate。同样类比UIViewController的话,可以将它们理解为分别对应-viewDidLoad,viewWillAppear:以及-viewDidDisappear:。虽然看方法名和实际使用上可能你会认为-initWithContext:应该对应UIViewController的init或者initWithCoder:这样的方法,但是事实上在-initWithContext:时WKInterfaceController中的“视图元素”(请注意这里我加上了引号,因为它们不是真正的视图,稍后会再说明)都已经初始化完毕可用,这其实和-viewDidLoad中的行为更加相似。
我们一般在-initWithContext:和-willActivate中配置“视图元素”的属性,在-didDeactivate中停用像是NSTimer之类的会hold住self的对象。需要特别注意的是,在-didDeactivate中对“视图元素”属性进行设置是无效的,因为当前的WKInterfaceController已经非活跃。
WKInterfaceObject 及其子类
WKInterfaceObject负责具体的界面元素设置,包括像是WKInterfaceButton,WKInterfaceLabel或WKInterfaceImage这类物件,也就是我们上面所提到的“视图元素”。可能一开始会产生错觉,觉得WKInterfaceObject应该对应UIView,但其实上并非如此。WKInterfaceObject只是 WatchKit 的实际的 View 的一个在 Watch Extension 端的代理,而非 View 本身。Watch App中实际展现和渲染在屏幕上的 view 对于代码来说是非直接可见的,我们只能在 Extension target中通过对应的代理对象对属性进行设置,然后在每个 run loop 需要刷新 UI 时由 WatchKit将新的属性值从手机中传递给手表中的 Watch App 并进行界面刷新。
反过来,手表中的实际的 view 想要将用户交互事件传递给 iPhone也需要通过WKInterfaceObject代理进行。每个可交互的WKInterfaceObject子类都对应了一个 action,比如 button 对应点击事件,switch 对应开或者关的状态,slider对应一个浮点数值表明选取值等等。关联这些事件也很简单,直接从 StoryBoard 文件中 Ctrl拖拽到实现中就能生成对应的事件了。虽然 UI 资源文件和代码实现是在不同的 target 中的,但是在 Xcode中的协作已然天衣无缝。
Watch App 采取的布局方式和 iOS app 完全不同。你无法自由指定某个视图的具体坐标,当然也不能使用像AutoLayout 或者 Size Classes 这样的灵活的界面布局方案。WatchKit提供的布局可能性和灵活性相对较小,你只能在以“行”为基本单位的同时通过 group来在行内进行“列”布局。这带来了相对简单的布局实现,当然,同时也是对界面交互的设计的一种挑战。
另外值得一提的是,随着 WatchKit 的出现及其开发方式的转变,代码写 UI 还是使用
StoryBoard这个争论了多年的话题可以暂时落下帷幕了。针对 Watch的开发不能使用代码的方式。首先,所有的WKInterfaceObject对象都必须要设计的时候经由StoryBoard 进行添加,运行时我们无法再向界面上添加或者移除元素(如果有移除需要的,可以使用隐藏);其次WKInterfaceObject与布局相关的某些属性,比如行高行数等,不能够在运行时进行变更和设定。基本来说在运行时我们只能够改变视图的内容,以及通过隐藏某些视图元素来达到有限地改变布局(其他视图元素会试图填充被隐藏的元素)。
总之,代码进行 UI 编写的传统,在 Apple 的不断努力下,于 WatchKit 发布的今天,被正式宣判了死刑。
Table 和 Context Menu
大部分WKInterfaceObject子类都很直接简单,但是有两个我想要单独说一说,那就是WKInterfaceTable和WKInterfaceMenu。UITableView大家都很熟悉了,在WatchKit中的WKInterfaceTable虽然也是用来展示一组数据,但是因为WatchKit API的数据传输的特点,使用上相较UITableView有很大不同和简化。首先不存在DataSource 和Delegate,WKInterfaceTable中需要呈现的数据数量直接由其实例方法-setNumberOfRows:withRowType:进行设定。在进行设定后,使用-rowControllerAtIndex:枚举所有的rowController进行设定。这里的rowController是在StoryBoard中所设定的相当于UITableViewCell的东西,只不过和其他WKInterfaceObject一样,它是直接继承自NSObject的。你可以通过自定义rowController并连接StoryBoard的元素,并在取得rowController对其进行设定,即可完成table 的显示。代码大概是这样的:
//MyRowController.swift
import Foundation
import WatchKit
class MyRowController: NSObject{
@IBOutlet
weak var label: WKInterfaceLabel!
}
//InterfaceController.swift
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController
{
@IBOutlet
weak var table: WKInterfaceTable!
let data = ["Index 0","Index 1","Index 2"]
override init(context: AnyObject?) {
// Initialize variables here.
super.init(context:
context)
// Configure interface objects here.
NSLog("%@init", self)
//注意需要在 StoryBoard 中设置 myRowControllerType
//类似 cell 的 reuse id
table.setNumberOfRows(data.count,withRowType: "myRowControllerType")
for(i, value) in enumerate(data) {
if let rowController = table.rowControllerAtIndex(i) as? MyRowController {
rowController.label.setText(value) }}}}
对于点击事件,并没有一个实际的 delegate存在,而是类似于其他WKInterfaceObject那样通过action 将点击了哪个 row作为参数发送回WKInterfaceController进行处理。
另一个比较好玩的是 Context Menu,这是 WatchKit 独有的交互,在 iOS中并不存在。在任意一个WKInterfaceController界面中,长按手表屏幕,如果当前WKInterfaceController中存在上下文菜单的话,就会尝试呼出找这个界面对应的 ContextMenu。这个菜单最多可以提供四个按钮,用来针对当前环境向用户征询操作。因为手表屏幕有限,在信息显示的同时再放一些交互按钮是挺不现实的一件事情,也会很丑。而上下文菜单很好地解决了这个问题,相信长按呼出交互菜单这个操作会成为今后 Watch App的一个很标准的交互操作。
添加 Context Menu 非常简单,在 StoryBoard里向WKInterfaceController中添加一个Menu,并在这个 Menu 里添加对应的 MenuItem就行了。在WKInterfaceController我们也有对应的API 来在运行时根据上下文环境进行 MenuItem 的添加 (这是少数几个允许我们在运行时添加元素的方法之一)。
-addMenuItemWithItemIcon:title:action:
-addMenuItemWithImageNamed:title:action:
-addMenuItemWithImage:title:action:
-clearAllMenuItems
但是 Menu 和 MenuItem对应的类WKInterfaceMenu和WKInterfaceMenuItem我们是没有办法拿到的。没错,它们甚至都没有存在于文档里:(
基础导航
WKInterfaceController的内建的导航关系基本上分为三类。首先是像UINavigationController控制的类似栈的导航方式。相关的API有-pushControllerWithName:context:,-popController以及-popToRootController。后两个我想不必太多解释,对于第一个方法,我们需要使用目标controller的Identifier字符串(没有你只能在 StoryBoard 里进行设置)进行创建。context参数也会被传递到目标 controller的-initWithContext:中,所以你可以以此来在controller 中进行数据传递。
另一种是我们大家熟悉的 modal 形式,对应 API是-presentControllerWithName:context:和-dismissController。对于这种导航,和UIKit中的不同之处就是在目标 controller 中会默认在左上角加上一个 Cancel 按钮,点击的话会直接关闭被 present 的controller。我只想说 Apple 终于想通了,每个 modal 出来的 controller都是需要关闭的这个事实…
最后一种导航方式是类似UIPageController的分页式导航。在iOS app 中,在应用第一次开始时的教学模块中这种导航方式非常常见,而在 WatchKit里可以说得到了发扬光大。事实上我个人也认为这会是 WatchKit 里最符合使用习惯的导航方式。在 WatchKit 上的 page导航可能会和 iOS app 的 Tab 导航所提供的功能相对应。
在实现上,page 导航需要在 StoryBoard 中用 segue 的方式将不同 page进行连接,新添加的nextpagesegue 就是干这个的:
另外 modal 导航的另一个API-presentControllerWithNames:contexts:接受复数个的names和context,通过这种方式modal 呼出的复数个 Controller 也将以 page 导航方式呈现。
当然,作为 StoryBoard 的经典使用方式,modal 和 push 的导航方式也是可以在 StoryBoard 中通过segue 来实现的。同时 WatchKit 也为 segue 的方式提供了必要的 API。
一些界面实践因为整个架构和UIKit完全不同,所以很多之前的实践是无法直接搬到WatchKit App 中的。
图像处理
在UIKit中我们显示图片一般使用UIImageView,然后为其image属性设置一个创建好的UIImage对象。而对于WatchKit 来说,最佳实践是将图片存放在 Watch App 的 target 中 (也就是 StoryBoard 的那个target),在对WKInterfaceImage进行图像设置时,尽量使用它的-setImageNamed:方法。这个方法将只会把图像名字通过手机传递到手表,然后由手表在自己的bundle 中寻找图片并加载,是最快的途径。注意我们的代码是运行在于手表的 Watch App不同的设备上的,虽然我们也可以先通过UIImage的相关方法生成UIImage对象,然后再用-setImage:或者-setImageData:来设置手表上的图片,但是这样的话我们就需要将图片放到Extension 的 target 中,并且需要将图片的数据通过蓝牙传到手表,一般来说这会造成不可忽视的延迟,会很影响体验。
如果对于某些情况下,我们只能在 Extension 的 target 中获得图片(比如从网络下载或者代码动态生成等),并且需要重复使用的话,最好用WKInterfaceDevice的-addCachedImage:name:方法将其缓存到手表中。这样,当我们之后再使用这张图片的时候就可以直接通过-setImageNamed:来快速地从手表上生成并使用了。每个app 的 cache 的尺寸大约是 20M,超过的话 WatchKit 将会从最老的数据开始删除,以腾出空间存储新的数据。
动画
因为无法拿到实际的视图元素,只有WKInterfaceObject这样的代理对象,以及布局系统的限制,所以复杂的动画,尤其是UIView系列或者是CALayer系列的动画是无法实现的。现在看来唯一可行的是帧动画,通过为WKInterfaceImage设置包含多个image 的图像,或者是通过计时器定时替换图像的话,可以实现帧动画。虽然 Apple自己的例子也通过这种方法实现了动画,但是对于设备的存储空间和能耗都可能会是挑战,还需要实际拿到设备以后进行实验和观察。
其他 Cocoa Touch 框架的使用
Apple 建议最好不要使用那些需要 prompt 用户许可的特性,比如 CoreLocation定位等。因为实际的代码是在手机上运行的,这类许可也会在手机上弹出,但是用户并不一定正好在看手机,所以很可能造成体验下降。另外大部分后台运行权限也是不建议的。对于要获取这些数据和权限,Apple 建议还是在 iOS app 中完成,并通过 App Groups 进行数据共享,从而在Watch Extension 中拿到这些数据。
代码分享
因为现在一个项目会有很多不同的 target,所以使用 framework 的方式封装不同 target的公用部分的代码,而只在各 target中实现界面相关的代码应该是必行的了。这么做的优点不仅是可以减少代码重复,也会使代码测试和品质得到提升。如果还没有进行逻辑部分的框架化和测试分离的话,在实现像各类 Extension 或者 Watch App 时可能会遇到非常多的麻烦。因为如果原有 app 有计划进行扩展推出各种 Extension 的话,将逻辑代码抽离并封装为 framework应该是优先级最高的工作。另外新开的项目如果没有特殊原因,也强烈建议使用 framework 来组织通用代码。
Glance 和 Notification除了 Watch App 本体以外,Glance 和手表的 Notification也是重要的使用情景。Notification 虽然概念上比较简单,但是相对于 iOS 的通知来说是天差地别。WatchKit的通知允许开发者自行构建界面,我们可以通过 payload设置比较复杂和带有更多信息的通知,包括图像,大段文字甚至可以交互的按钮,而不是像 iOS上那样被限制在文字和一个对话框内。首先无论是通过 Local 还是 Remote 进行的通知发送会先到达 iPhone,然后再由iPhone 根据内容判断是否转发到手表。WatchKit App 接收到通知后先会显示一个简短的通知,告诉用户这个 app有一个通知。如果用户对通知的内容感兴趣的话,可以点击或者抬手观看,这样由开发者自定义的长版本的通知就会显现。
Glance 是 WatchKit 的新概念,它允许 Watch App展示一个布局固定的WKInterfaceController页 面。它和Watch App 本体相对地位相当于 iOS 上的 Today Widget 和 iOS app 本身的地位,是作为手表上的 app的最重要的信息展示出现的。Glance 正如其名,是短时存在的提醒,不能存在可交互的元素。不过如果用户点击 Glance页面的话,是可以启动到 Watch App 的。现在关于 Glance 本身如何启动和呈现还不是很明确,猜测是某种类似 TodayWidget 的呈现方式?(比如按下两次表侧面的旋钮)
疑问和改进方向
WatchKit 总体令人满意,提供的 API和开发环境已经足够开发者作出一些有趣的东西。但是有几个现在看来很明显的限制和希望能加强的方向。首先是从现在来看 WatchKit 并没有提供任何获取设备传感信息的 API。不论是心跳、计步或者是用户是否正在佩戴 Watch的信息我们都是拿不到的,这限制了很多数据收集和监视的健康类 app 的制作。如果希望请求数据,还是不得不转向使用HealthKit。但是随着 iPhone 6 和 6s 的大屏化,在运动时携带 iPhone 的人可以说是变少了。如果 Watch不能在没有 iPhone 配对的情况下收集记录,并在之后和 iPhone 连接后将数据回传的话,那 Apple的健康牌就失败了一大半。相信 Apple不会放过这种把用户捆绑的机会…不过如果第三方应用能实时获取用户的佩戴状况的话,相信会有很多有意思的应用出现。
另外作为在发布会上鼓吹的交互革命的旋钮和触感屏幕,现在看来并没有开放任何 API供开发者使用,所以我们无法得知用户旋转了手表旋钮这个重要的交互事件。现在看来我们能获取的操作仅只是用户点击屏幕上的按钮或者拖动滑条这个层级,从这个角度来说,现在的 WatchKit 还远没达到可以颠覆移动应用的地步。希望之后 Apple 会给我们带来其他的好消息吧。
总之,舞台已经搭好,之后唱什么戏,就要看我们的了。