转自http://onevcat.com/2014/11/watch-kit/发现一个写的非常好的人,转一下 以后持续关注
主要类
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
与布局相关的某些属性,比如行高行数等,不能够在运行时进行变更和设定。基本来说在运行时我们只能够改变视图的内容,以及通过隐藏某些视图元素来达到有限地改变布局 (其他视图元素会试图填充被隐藏的元素)。
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
中存在上下文菜单的话,就会尝试呼出找这个界面对应的 Context Menu。这个菜单最多可以提供四个按钮,用来针对当前环境向用户征询操作。因为手表屏幕有限,在信息显示的同时再放一些交互按钮是挺不现实的一件事情, 也会很丑。而上下文菜单很好地解决了这个问题,相信长按呼出交互菜单这个操作会成为今后 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 进行连接,新添加的 next page
segue 就是干这个的:
另外 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 中拿到这些数据。