Apple Watch 和 watchOS 第一代产品只允许用户在 iPhone 设备上进行计算,然后将结果传输到手表上进行显示。在这个框架下,手表充当的功能在很大程度上只是手机的另一块小一些的显示器。而在 watchOS 2 中,Apple 开放了在手表端直接进行计算的能力,一些之前无法完成的 app 现在也可以进行构建了。本文将通过一个很简单的天气 app 的例子,讲解一下 watchOS 2 中新引入的一些特性的使用方法。
本文是我的 WWDC15 笔记中的一篇,在 WWDC15 中涉及到 watchOS 2 的相关内容的 session 非常多,本文所参考的有:
- Introducing WatchKit for watchOS 2
- WatchKit In-Depth, Part 1
- WatchKit In-Depth, Part 2
- Introducing Watch Connectivity
- Building Watch Apps
- Creating Complications with ClockKit
项目简介
作为一个示例项目,我们就来构建一个最简单的天气 app 吧。本文将一步步带你从零开始构建一个相对完整的 iOS + watch app。这个 app 的 iOS 端很简单,从数据源取到数据,然后解析成天气的 model 后,通过一个 PageViewController 显示出来。为了让 demo 更有说服力,我们将展示当前日期以及前后两天的天气情况,包括天气状况和气温。在手表端,我们希望构建一个类似的 app,可以展示这几天的天气情况。另外我们当然也介绍如何利用 watchOS 2 的一些新特性,比如 complications 和 Time Travel 等等。
开始
虽然本文的重点是 watchOS,但是为了完整性,我们还是从开头开始来构建这个 app 吧。因为不管是 watchOS 1 还是 2,一个手表 app 都是无法脱离手机 app 单独存在和申请的。所以我们首先来做的是一个像模像样的 iOS app 吧。
新建项目
第一步当然是使用 Xcode 7 新建一个工程,这里我们直接选择 iOS App with WatchKit App,这样 Xcode 将直接帮助我们建立一个带有 watchOS app 的 iOS 应用。
在接下来的画面中,我们选中 Include Complication 选项,因为我们希望制作一个包含有 Complication 的 watch app。
UI
这个 app 的 UI 部分比较简单,我将使用到的素材都放到了这里。你可以下载这些素材,并把它们解压并拖拽到项目 iOS app 的 Assets.xcassets 里去:
接下来,我们来构建 UI 部分。我们想要使用 PageViewController 来作为 app 的导航,首先,在 Main.StoryBoard 中删掉原来的 ViewController,并新加一个 Page View Controller,然后在它的 Attributes Inspector 中将 Transition Style 改为 Scroll,并勾选上 Is Initial View Controller。这将使这个 view controller 成为整个 app 的入口。
接下来,我们需要将这个 Page View Controller 和代码关联起来。首先将 ViewController.swift 文件中,将 ViewController 的继承关系从 UIViewController
改为 UIPageViewController
。
class ViewController: UIPageViewController {
...
}
然后我们就可以在 StoryBoard 文件中将刚才的 Page View Controller 的 class 改为我们的 ViewController
了。
另外我们还需要一个实际展示天气的 View Controller。创建一个继承自 UIViewController
的WeatherViewController
,然后将 WeatherViewController.swift 的内容替换为:
import UIKit
class WeatherViewController: UIViewController {
enum Day: Int {
case DayBeforeYesterday = -2
case Yesterday
case Today
case Tomorrow
case DayAfterTomorrow
}
var day: Day?
}
这里仅只是定义了一个 Day
的枚举,它将用来标记这个 WeatherViewController
所代表的日期 (可能你会说把 Day
在 ViewController 里并不是很好的选择,没错,但是放在这里有助于我们快速搭建 app,在之后我们会对此进行重构)。接下来,我们在 StoryBoard 中添加一个 ViewController,并将它的 class 改为 WeatherViewController
。我们可以在这里构建 UI,对于这个 demo 来说,一个简单的背景,加上表示天气的图标和表示温度的标签就足够了。因为这里并不是一个关于 Auto Layout 或是 Size Class 的 demo,所以就不详细一步步地做了,我随意拖了拖 UI 和约束,最后结果如下图所示。
接下来就是从 StoryBoard 中把需要的 IBOutlet 拖出来。我们需要天气图标,最高最低温度的 label。完成这些 UI 工作之后的项目可以在 GitHub 的这个 tag 下找到,如果你不想自己完成这些步骤的话,也可以直接使用这个 tag 的源文件来继续下面的 demo。当然,如果你对 AutoLayout 和 Interface Builder 还不熟悉的话,这会是一个很好的机会来从简单的布局入手,去理解对 IB 的使用。关于更多 IB 和 StoryBoard 的教程,推荐 Raywenderlich 的这两篇系列文章:Storyboards Tutorial in Swift 和 Auto Layout Tutoria。
然后我们可以考虑先把 Page View Controller 的框架实现出来。在 ViewController.swift
中,我们首先在ViewController
类中加入以下方法:
func weatherViewControllerForDay(day: WeatherViewController.Day) -> UIViewController {
let vc = storyboard?.instantiateViewControllerWithIdentifier("WeatherViewController") as! WeatherViewController
let nav = UINavigationController(rootViewController: vc)
vc.day = day
return nav
}
这将从当前的 StroyBoard 里寻找 id 为 "WeatherViewController" 的 ViewController,并且初始化它。我们希望能为每一天的天气显示一个 title,一个比较理想的做法就是直接将我们的 WeatherViewController 嵌套在 navigation controller 里,这样我们就可以直接使用 navigation bar 来显示标题,而不用去操心它的布局了。我们刚才并没有为WeatherViewController
指定 id,在 StoryBoard 中,找到 WeatherViewController,然后在 Identity 里添加即可:
接下来我们来实现 UIPageViewControllerDataSource
。在 ViewController.swift
的 viewDidLoad
里加入:
dataSource = self
let vc = weatherViewControllerForDay(.Today)
setViewControllers([vc], direction: .Forward, animated: true, completion: nil)
首先它将 viewController
自己设置为 dataSource。然后设定了初始需要表示的 viewController。对于UIPageViewControllerDataSource
的实现,我们在同一文件中加入一个 ViewController
的 extension 来搞定:
extension ViewController: UIPageViewControllerDataSource {
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
guard let nav = viewController as? UINavigationController,
viewController = nav.viewControllers.first as? WeatherViewController,
day = viewController.day else {
return nil
}
if day == .DayBeforeYesterday {
return nil
}
guard let earlierDay = WeatherViewController.Day(rawValue: day.rawValue - 1) else {
return nil
}
return self.weatherViewControllerForDay(earlierDay)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
guard let nav = viewController as? UINavigationController,
viewController = nav.viewControllers.first as? WeatherViewController,
day = viewController.day else {
return nil
}
if day == .DayAfterTomorrow {
return nil
}
guard let laterDay = WeatherViewController.Day(rawValue: day.rawValue + 1) else {
return nil
}
return self.weatherViewControllerForDay(laterDay)
}
}
这两个方法分别根据输入的 View Controller 对象来确定前一个和后一个 View Controller,如果返回 nil
则说明没有之前/后的页面了。另外,我们可能还想要先将 title 显示出来,以确定现在的架构是否正确工作。在WeatherViewController.swift
的 Day 枚举里添加如下属性:
var title: String {
let result: String
switch self {
case .DayBeforeYesterday: result = "前天"
case .Yesterday: result = "昨天"
case .Today: result = "今天"
case .Tomorrow: result = "明天"
case .DayAfterTomorrow: result = "后天"
}
return result
}
然后将 day
属性改为:
var day: Day? {
didSet {
title = day?.title
}
}
运行 app,现在我们应该可以在五个页面之间进行切换了。你也可以从 GitHub 上对应的 tag 中下载到目前为止的项目。
重构和 Model
很难有人一次性就把代码写得完美无瑕,这也是重构的意义。重构从来不是一个“等待项目完成后再开始”的活动,而是应该随着项目的展开和进行,一旦发现有可能存在问题的地方,就尽快进行改进。比如在上面我们将 Day
放在了WeatherViewController
中,这显然不是一个很好地选择。这个枚举更接近于 Model 层的东西而非控制层,我们应该将它迁移到另外的地方。同样现在还需要实现的还有天气的 Model,即表征天气状况和高低温度的对象。我们将这些内容提取出来,放到一个 framework 中去,以便使用的维护。
我们首先对现有的 Day
进行迁移。创建一个新的 Cocoa Touch Framework target,命名为 WatchWeatherKit
。在这个 target 中新建 Day.swift
文件,其中内容为:
public enum Day: Int {
case DayBeforeYesterday = -2
case Yesterday
case Today
case Tomorrow
case DayAfterTomorrow
public var title: String {
let result: String
switch self {
case .DayBeforeYesterday: result = "前天"
case .Yesterday: result = "昨天"
case .Today: result = "今天"