-
Notifications
You must be signed in to change notification settings - Fork 317
Creating a watchOS App
本教程为你提供一个将你已经学到的关于
SwiftUI
的知识应用到自己的产品上的机会,并且不费吹灰之力就可以将 Landmarks app 迁移到 watchOS 上。首先,给项目添加一个 watchOS target,然后复制为 iOS app 中创建的共享数据和视图。当所有资源都准备好后,你就可以通过自定义 SwiftUI 视图,在 watchOS 上显示详细信息和列表视图。
下载项目文件并按照以下步骤操作,也可以打开已完成的项目自行浏览代码。
- 预计完成时间:25 分钟
- 项目文件:下载
要创建 watchOS app,首先要给项目添加一个 watchOS target。
Xcode 会将 watchOS app 的组和文件,以及构建和运行 app 所需的 scheme 添加到项目中。
1.1 选择 File
> New
> Target
,当模版表单显示后,选择 watchOS
标签,选择 Watch App for iOS App
模版后点击 Next
。
这个模版会给项目添加一个新的 watchOS app,将 iOS app 与它配对。
1.2 在表单的 Product Name
中输入 WatchLandmarks
,将 Language
设置成 Swift
,将 User Interface
设置成 SwiftUI
。勾选 Include Notification Scene
复选框,然后点击 Finish
。
1.3 Xcode 弹出提示,点击 Activate
。
这样选择 WatchLandmarks
scheme 后,就可以构建和运行你的 watchOS app 了。
Whenever possible, create an independent watchOS app. Independent watchOS apps don’t require an iOS companion app.
1.4 在 WatchLandmarks Extension
的 General
标签中,勾选 Supports Running Without iOS App Installation
复选框。
尽可能创建一个独立的 watchOS app。独立的 watchOS app 不需要与 iOS app 配套使用。
设置了 watchOS target 后,你需要从 iOS target 中共享一些资源。比如重用 Landmark app 中的数据模型,一些资源文件,以及任何不需要修改就可以跨平台显示的视图。
2.1 在项目导航器中,按住 Command
键然后点击选中以下文件:LandmarkRow.swift
, Landmark.swift
, UserData.swift
, Data.swift
, Profile.swift
, Hike.swift
, CircleImage.swift
。
Landmark.swift
, UserData.swift
, Data.swift
, Profile.swift
, Hike.swift
定义了 app 的数据模型。虽然你不会用到所有这些模型,但是需要保证这些文件都编译到了 app 中。 LandmarkRow.swift
和 CircleImage.swift
是两个不修改就可以显示在 watchOS 中的视图。
2.2 打开 File
检查器,勾选 Target Membership
中的 WatchLandmarks Extension
复选框。
这会让你在上一步中选择的文件在 watchOS app 中可用。
2.3 打开项目导航器,在 Landmark
组中选择 Assets.xcassets
,然后在 File
检查器的 Target Membership
中将它添加到 WatchLandmarks
target。
这与你上一步选择到 target 不一样, WatchLandmarks Extension
target 包含你的 app 的代码,而 WatchLandmarks
target 则管理你的故事板,图标和相关资源。
2.4 在项目导航器中,选择 Resources
文件夹中的所有文件,然后在 File
检查器的 Target Membership
中将它们添加到 WatchLandmarks Extension
target。
现在 iOS target 的资源在 watch app 上已经可用了,你需要创建一个 watch 独有的视图来显示地标详情。为了测试这个视图,你需要给最大和最小 watch 尺寸创建自定义预览,然后给圆形视图做一些修改来适配 watch 显示。
3.1 在项目导航器中,单击 WatchLandmarks Extension
文件夹旁边的显示三角形来显示其内容,然后添加一个新 SwiftUI
视图,命名为 WatchLandmarkDetail
。
3.2 给 WatchLandmarkDetail
结构体添加 userData
, landmark
和 landmarkIndex
属性。
这些和你在 处理用户输入 中添加到 LandmarkDetail
结构体中的属性是一样的。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
//
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
//
var body: some View {
Text("Hello World!")
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
WatchLandmarkDetail()
}
}
在上一步添加属性后,你会在 Xcode 中得到一个缺少参数的错误。为了修复这个错误,你需要二选一:提供属性的默认值,或传递参数来设置视图的属性。
3.3 在预览中,创建一个用户数据的实例,然后用它给 WatchLandmarkView
结构体的初始化传递一个地标对象。另外还需要将这个用户数据设置成视图的环境对象。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
Text("Hello World!")
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
//
let userData = UserData()
return WatchLandmarkDetail(landmark: userData.landmarks[0])
.environmentObject(userData)
//
}
}
3.4 在 WatchLandmarkDetail.swift
中,从 body()
方法里返回一个 CircleImage
视图。
这就是你从 iOS 项目中复用 CircleImage
视图的地方。因为创建了可调整大小的图片, .scaledToFill()
的调用会让圆形的尺寸自动适配显示。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
//
CircleImage(image: self.landmark.image.resizable())
.scaledToFill()
//
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return WatchLandmarkDetail(landmark: userData.landmarks[0])
.environmentObject(userData)
}
}
3.5 给最大 (44mm) 和最小 (38mm) 表盘创建预览。
通过针对最大和最小表盘的测试,你可以看到你的 app 是如何缩放来适配显示的。与往常一样,你应该在所有支持的设备尺寸上测试用户界面。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
CircleImage(image: self.landmark.image.resizable())
.scaledToFill()
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
//
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
//
}
}
圆形图片重新调整大小来适配显示的高度。但不幸,这依然裁剪了圆形的宽度。为了修复这个裁剪问题,你需要把图片嵌入到一个 VStack
中,并且做一些额外的布局修改来让圆形图片适配任何 watch 的宽度。
3.6 把图片嵌入到一个 VStack
中,在图片下面显示地标的名字和它的信息。
如你所见,信息并没有完全适配 watch 的屏幕,但是你可以通过将这个 VStack
放在一个滚动视图中来修复这个问题。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
//
VStack {
CircleImage(image: self.landmark.image.resizable())
.scaledToFill()
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
}
//
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
3.7 将竖直 stack 包装中一个滚动视图中。
这让视图可以滚动,但是带来了另外一个问题:圆形图片展开到了全屏,并且调整了其他 UI 元素来匹配这个图片。你需要调整这个圆形图片的大小来让它和地标名字显示在屏幕上。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
//
ScrollView {
VStack {
CircleImage(image: self.landmark.image.resizable())
.scaledToFill()
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
}
}
//
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
3.8 把 scaleToFill()
改成 scaleToFit()
。
这会让圆形图片缩放来匹配显示的宽度。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
VStack {
CircleImage(image: self.landmark.image.resizable())
//
.scaledToFit()
//
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
}
}
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
3.9 添加填充使地标名字在圆形图像下方可见。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
VStack {
CircleImage(image: self.landmark.image.resizable())
.scaledToFit()
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
}
//
.padding(16)
//
}
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
3.10 给返回按钮添加一个标题。
这里将返回按钮的文字设置成来 Landmarks
,但是在本教程后面的部分中,只有添加 LandmarksList
视图后,你才能看到返回按钮。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
VStack {
CircleImage(image: self.landmark.image.resizable())
.scaledToFit()
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
}
.padding(16)
}
//
.navigationBarTitle("Landmarks")
//
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
现在你已经创建了基本的详情视图,可以添加地图视图来显示地标的位置了。与 CircleImage
不同,你不能仅仅重用 iOS app 的 MapView
。相对的,你需要创建一个 WKInterfaceObjectRepresentable
结构体来包装 WatchKit 地图。
4.1 给 WatchKit extension
添加一个自定义视图,命名为 WatchMapView
。
WatchMapView.swift
import SwiftUI
struct WatchMapView: View {
var body: some View {
Text("Hello World!")
}
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
WatchMapView()
}
}
4.2 在 WatchMapView
结构体中,将 View
改成 WKInterfaceObjectRepresentable
。
在步骤 1 和 2 所示的代码之间来回滚动来查看区别。
WatchMapView.swift
import SwiftUI
//
struct WatchMapView: WKInterfaceObjectRepresentable {
//
var body: some View {
Text("Hello World!")
}
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
WatchMapView()
}
}
Xcode 会显示编译错误,因为 WatchMapView
还没有遵循 WKInterfaceObjectRepresentable
属性。
4.3 删除 body()
方法,将其替换为 landmark
属性。
每当你创建一个地图视图你都需要给这个属性传递一个值。比如,你可以给预览传递一个地标实例。
WatchMapView.swift
import SwiftUI
struct WatchMapView: WKInterfaceObjectRepresentable {
//
var landmark: Landmark
//
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
//
WatchMapView(landmark: UserData().landmarks[0])
//
}
}
4.4 实现 WKInterfaceObjectRepresentable
协议的 makeWKInterfaceObject(context:)
方法。
这个方法会创建 WatchMapView
用来显示的 WatchKit 地图。
WatchMapView.swift
import SwiftUI
struct WatchMapView: WKInterfaceObjectRepresentable {
var landmark: Landmark
//
func makeWKInterfaceObject(context: WKInterfaceObjectRepresentableContext<WatchMapView>) -> WKInterfaceMap {
return WKInterfaceMap()
}
//
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
WatchMapView(landmark: UserData().landmarks[0])
}
}
4.5 实现 WKInterfaceObjectRepresentable
协议的 updateWKInterfaceObject(_:, context:)
方法,根据地标坐标设置地图的范围。
现在项目可以成功构建而没有任何错误了。
WatchMapView.swift
import SwiftUI
struct WatchMapView: WKInterfaceObjectRepresentable {
var landmark: Landmark
func makeWKInterfaceObject(context: WKInterfaceObjectRepresentableContext<WatchMapView>) -> WKInterfaceMap {
return WKInterfaceMap()
}
//
func updateWKInterfaceObject(_ map: WKInterfaceMap, context: WKInterfaceObjectRepresentableContext<WatchMapView>) {
let span = MKCoordinateSpan(latitudeDelta: 0.02,
longitudeDelta: 0.02)
let region = MKCoordinateRegion(
center: landmark.locationCoordinate,
span: span)
map.setRegion(region)
}
//
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
WatchMapView(landmark: UserData().landmarks[0])
}
}
4.6 选中 WatchLandmarkView.swift
文件,然后把地图视图添加到竖直 stack 的底部。
代码在地图视图之后添加了一个分割线。.scaledToFit()
和 .padding()
修饰符让地图的尺寸很好的匹配了屏幕。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
VStack {
CircleImage(image: self.landmark.image.resizable())
.scaledToFit()
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
//
Divider()
WatchMapView(landmark: self.landmark)
.scaledToFit()
.padding()
//
}
.padding(16)
}
.navigationBarTitle("Landmarks")
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
对于地标列表,你可以重用 iOS app 中的行视图,但是每个平台需要展示其自身的详情视图。为此,你需要将明确定义详情视图的 LandmarkList
视图转换为范型列表类型,
5.1 在工具栏中,选中 Landmarks scheme
。
Xcode 现在会构建和运行 app 的 iOS 版本。在把列表移动到 watchOS app 之前,你需要确认任何对 LandmarkList
视图对修改在 iOS app 中依然生效。
5.2 选中 LandmarkList.swift
然后修改类型的声明将其变成范型类型。
LandmarksList.swift
import SwiftUI
//
struct LandmarkList<DetailView: View>: View {
//
@EnvironmentObject private var userData: UserData
var body: some View {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
destination: LandmarkDetail(landmark: landmark).environmentObject(self.userData)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
}
.environmentObject(UserData())
}
}
添加范型声明会让你无论何时创建一个 LandmarkList
结构体实例时都会出现 Generic parameter could not be inferred
错误。接下来的几步会修复这些错误。
5.3 添加一个创建详情视图的闭包属性。
LandmarksList.swift
import SwiftUI
struct LandmarkList<DetailView: View>: View {
@EnvironmentObject private var userData: UserData
//
let detailViewProducer: (Landmark) -> DetailView
//
var body: some View {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
destination: LandmarkDetail(landmark: landmark).environmentObject(self.userData)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
}
.environmentObject(UserData())
}
}
5.4 使用 detailViewProducer
属性给地标创建详情视图。
LandmarksList.swift
import SwiftUI
struct LandmarkList<DetailView: View>: View {
@EnvironmentObject private var userData: UserData
let detailViewProducer: (Landmark) -> DetailView
var body: some View {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
//
destination: self.detailViewProducer(landmark).environmentObject(self.userData)) {
//
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
}
.environmentObject(UserData())
}
}
当你创建了一个 LandmarkList
的实例后,你还需要提供一个给地标创建详情视图的闭包。
5.5 选中 Home.swift
,在 CategoryHome
结构体的 body()
方法中添加一个闭包来创建 LandmarkDetail
视图。
Xcode 会根据闭包的返回类型来推断 LandmarkList
结构体的类型。
Home.swift
import SwiftUI
struct CategoryHome: View {
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarkData,
by: { $0.category.rawValue }
)
}
var featured: [Landmark] {
landmarkData.filter { $0.isFeatured }
}
@State var showingProfile = false
var profileButton: some View {
Button(action: { self.showingProfile.toggle() }) {
Image(systemName: "person.crop.circle")
.imageScale(.large)
.accessibility(label: Text("User Profile"))
.padding()
}
}
var body: some View {
NavigationView {
List {
FeaturedLandmarks(landmarks: featured)
.scaledToFill()
.frame(height: CGFloat(200))
.clipped()
.listRowInsets(EdgeInsets())
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: self.categories[key]!)
}
.listRowInsets(EdgeInsets())
//
NavigationLink(destination: LandmarkList { LandmarkDetail(landmark: $0) }) {
//
Text("See All")
}
}
.navigationBarTitle(Text("Featured"))
.navigationBarItems(trailing: profileButton)
.sheet(isPresented: $showingProfile) {
ProfileHost()
}
}
}
}
struct FeaturedLandmarks: View {
var landmarks: [Landmark]
var body: some View {
landmarks[0].image.resizable()
}
}
// swiftlint:disable type_name
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
.environmentObject(UserData())
}
}
5.6 在 LandmarkList.swift
中,给预览添加类似的代码。
在这里,你需要使用条件编译来根据 Xcode 的当前 scheme 来定义详细视图。Landmark app 现在可以按预期在 iOS 上构建并运行了。
LandmarksList.swift
import SwiftUI
struct LandmarkList<DetailView: View>: View {
@EnvironmentObject private var userData: UserData
let detailViewProducer: (Landmark) -> DetailView
var body: some View {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
destination: self.detailViewProducer(landmark).environmentObject(self.userData)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
#if os(watchOS)
typealias PreviewDetailView = WatchLandmarkDetail
#else
typealias PreviewDetailView = LandmarkDetail
#endif
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList { PreviewDetailView(landmark: $0) }
.environmentObject(UserData())
}
}
现在你已经更新了 LandmarksList
视图让其能在两个平台上都工作,可以将它添加到 watchOS app 中了。
6.1 在文件检查器中,把 LandmarksList.swift
添加到 WatchLandmarks Extension
target 中。
你现在可以中你的 watchOS app 的代码中使用 LandmarkList
视图了。
6.2 在工具栏中,将 scheme 改为 Watch Landmarks
。
6.3 打开 LandmarkList.swift
的同时,恢复预览。
现在预览会显示 watchOS 的列表视图。
LandmarksList.swift
import SwiftUI
struct LandmarkList<DetailView: View>: View {
@EnvironmentObject private var userData: UserData
let detailViewProducer: (Landmark) -> DetailView
var body: some View {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
destination: self.detailViewProducer(landmark).environmentObject(self.userData)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
#if os(watchOS)
typealias PreviewDetailView = WatchLandmarkDetail
#else
typealias PreviewDetailView = LandmarkDetail
#endif
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList { PreviewDetailView(landmark: $0) }
.environmentObject(UserData())
}
}
watchOS app 的根是显示默认 Hello World!
消息的 ContentView
。
6.4 修改 ContentView
来让它显示列表视图。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
LandmarkList { WatchLandmarkDetail(landmark: $0) }
.environmentObject(UserData())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
LandmarkList { WatchLandmarkDetail(landmark: $0) }
.environmentObject(UserData())
}
}
6.5 在模拟器中构建并运行 watchOS app。
通过在列表中滚动地标,点击视图的地标详情,并将其标记为收藏来测试 watchOS app 的行为。点击返回按钮回到列表,然后打开 Favorite
开关来查看收藏的地标。
你的 Landmarks 的 watchOS 版本差不多要完成了。在这最后一节,你需要创建一个通知界面来显示地标信息,当你在某一个收藏的地标附近时,就会收到这个通知。
注意
这一节只包含当你收到通知后如何显示,并不描述如何设置或发送通知。
7.1 打开 NotificationView.swift
并创建一个视图来显示地标的信息,标题和消息。
因为任何通知的值都可以为 nil,所以预览会显示通知的两个版本。第一个仅显示当没有数据时的默认值,第二个显示你提供的标题,信息,和位置。
NotificationView.swift
import SwiftUI
struct NotificationView: View {
//
let title: String?
let message: String?
let landmark: Landmark?
init(title: String? = nil,
message: String? = nil,
landmark: Landmark? = nil) {
self.title = title
self.message = message
self.landmark = landmark
}
//
var body: some View {
//
VStack {
if landmark != nil {
CircleImage(image: landmark!.image.resizable())
.scaledToFit()
}
Text(title ?? "Unknown Landmark")
.font(.headline)
.lineLimit(0)
Divider()
Text(message ?? "You are within 5 miles of one of your favorite landmarks.")
.font(.caption)
.lineLimit(0)
}
//
}
}
struct NotificationView_Previews: PreviewProvider {
//
//
static var previews: some View {
//
Group {
NotificationView()
NotificationView(title: "Turtle Rock",
message: "You are within 5 miles of Turtle Rock.",
landmark: UserData().landmarks[0])
}
.previewLayout(.sizeThatFits)
//
}
}
7.2 打开 NotificationController
并添加 landmark
, title
,和 message
属性。
这些数据存储发送进来的通知的相关值。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class NotificationController: WKUserNotificationHostingController<NotificationView> {
//
var landmark: Landmark?
var title: String?
var message: String?
//
override var body: NotificationView {
NotificationView()
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
}
}
7.3 更新 body()
方法来使用这些属性。
此方法会实例化你之前创建的通知视图。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class NotificationController: WKUserNotificationHostingController<NotificationView> {
var landmark: Landmark?
var title: String?
var message: String?
override var body: NotificationView {
//
NotificationView(title: title,
message: message,
landmark: landmark)
//
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
}
}
7.4 定义 LandmarkIndexKey
。
你需要使用这个键从通知中提取额地标的索引。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class NotificationController: WKUserNotificationHostingController<NotificationView> {
var landmark: Landmark?
var title: String?
var message: String?
//
let landmarkIndexKey = "landmarkIndex"
//
override var body: NotificationView {
NotificationView(title: title,
message: message,
landmark: landmark)
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
}
}
7.5 更新 didReceive(_:)
方法从推送中解析数据。
这个方法会更新控制器的属性。调用这个方法后,系统会使控制器的 body
属性无效,从而更新导航视图。然后系统会在 Apple Watch 上显示通知。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class NotificationController: WKUserNotificationHostingController<NotificationView> {
var landmark: Landmark?
var title: String?
var message: String?
let landmarkIndexKey = "landmarkIndex"
override var body: NotificationView {
NotificationView(title: title,
message: message,
landmark: landmark)
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
//
let userData = UserData()
let notificationData =
notification.request.content.userInfo as? [String: Any]
let aps = notificationData?["aps"] as? [String: Any]
let alert = aps?["alert"] as? [String: Any]
title = alert?["title"] as? String
message = alert?["body"] as? String
if let index = notificationData?[landmarkIndexKey] as? Int {
landmark = userData.landmarks[index]
}
//
}
}
当 Apple Watch 收到通知后,它会创建通知分类关联当通知控制器。你需要打开并编辑 app 当故事板来给你当通知控制器设置分类。
7.6 这项目导航器中,选中 Watch Landmarks
文件夹,打开 Interface
故事板。在故事板中选择指向静态通知界面控制器的箭头。
7.7 在 Attributes
检查器中,将 Notification Category
的 Name
设置成 LandmarkNear
。
配置测试载荷来使用 LandmarkNear
分类,并传递通知控制器期望的数据。
7.8 选择 PushNotificationPayload.apns
文件,然后更新 title
, body
, category
和 landmarkIndex
属性。确认将分类设置成了 LandmarkNear
。另外,删除在教程中任何没有用到的键,比如 subtitle
, WatchKit Simulator Actions
,以及 customKey
。
载荷文件会模拟从服务发来的远程通知数据。
PushNotificationPayload.apns
{
"aps": {
"alert": {
"body": "You are within 5 miles of Silver Salmon Creek."
"title": "Silver Salmon Creek",
},
"category": "LandmarkNear",
"thread-id": "5280"
},
"landmarkIndex": 1
}
7.9 选择 Landmarks-Watch (Notification)
scheme,然后构建并运行你的 app。
当你第一次运行通知 scheme,系统会请求发送通知的权限。选择 Allow
。之后模拟器会显示一个可滚动的通知,它包括:用于标记 Landmarks app 为发送方的框格,通知视图以及用于通知操作的按钮。
SwiftUI 纲要 - 绘制与动画 - App 设计与布局 - 框架集成