02 Creating A watchOS App - ly918/SwiftUI-Chinese-Documents GitHub Wiki
框架集成
创建watchOS应用程序
本教程为您提供了一个机会,让您可以应用您已经了解的有关SwiftUI的大部分内容,并且只需很少的努力将Landmarks
应用程序迁移到watchOS
。
在复制为iOS应用程序创建的共享数据和视图之前,您将首先向项目中添加watchOS target
。所有资源就绪后,您将自定义SwiftUI视图,以便在watchOS
上显示详情视图和列表视图。
学习时间:25分钟
第一节 添加watchOS Target
要创建watchOS应用程序,请首先将watchOS Target添加到项目中。Xcode将watchOS应用程序的文件夹和文件,以及构建和运行该应用程序所需的方案添加到项目中。
步骤1
选择File > New > Target
。当工作模板表出现时,选择watchOS
选项卡,选择Watch App for iOS
应用程序模板,然后单击Next
。
此模板将新的watchOS应用程序添加到您的项目中,并与iOS应用程序配对。
步骤2
在表单中,输入WatchLandmarks
作为产品名称。将语言设置为Swift
,将用户界面设置为SwiftUI
。选中Include Notification Scene
复选框,然后单击“Finish”。
步骤3
如果Xcode提示,请单击“Activate”。
这将选择WatchLandmarks
方案,以便您可以构建和运行watchOS应用程序。
步骤4
在WatchLandmarks
扩展的“General”选项卡中,选中Supports Running Without iOS App Installation(支持在不安装iOS应用程序的情况下运行)
复选框。
尽可能创建一个独立的watchOS应用程序。独立的watchOS应用不需要iOS配套应用。
第二节 在目标之间共享文件
设置了watchOS目标后,需要共享iOS目标中的一些资源。您将重用Landmark
应用程序的数据模型、一些资源文件以及两个平台都可以显示而无需修改的任何视图。
步骤1
在项目导航器中,单击命令以选择以下文件:LandmarkRow.swift、Landmark.swift、UserData.swift、Data.swift、Profile.swift、Hike.swift和CircleImage.swift。
Landmark.swift、UserData.swift、Data.swift、Profile.swift和Hike.swift定义了应用程序的数据模型。您不会使用模型的所有方面,但需要所有文件才能成功编译应用程序。LandmarkRow.swift和CircleImage.swift都是应用程序可以在watchOS上显示的视图,无需任何更改。
步骤2
在“文件检查器”中,选中“Target Membership”部分中的“WatchLandmarks Extension”复选框。
这使您在上一步中选择的文件可用于watchOS应用程序。
步骤3
在项目导航器中,选择Landmark组中的Assets.xclassets文件,并将其添加到File
检查器的Target Membership
部分中的WatchLandmarks
Target中。
这与您在上一步中选择的Target
不同。WatchLandmarks Extension
Target包含你的应用程序的代码,而WatchLandmarks Target
管理脚本、图标和相关资源。
步骤4
在项目导航器中,选择Resources
文件夹中的所有文件,然后在File
检查器的Target Membership
中将它们添加到WatchLandmarks Extension
target中。
第三节 创建详情视图
现在iOS目标资源已经准备好用于watch应用程序,您需要创建一个watch的视图来显示地标详情。为了测试详情视图,您将为最大和最小的手表尺寸创建自定义预览,并对圆形视图进行一些更改,以便所有内容都适合手表界面。
步骤1
在项目导航器中,单击WatchLandmarks Extension
文件夹旁边的三角形以显示其内容,然后添加名为WatchLandmarkDetail
的新SwiftUI视图。
步骤2
将userData、landmark、landmarkIndex
属性添加到WatchLandmarkDetail
结构中。
这些属性与您在Handling User Input时添加到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
在预览中,创建用户数据的实例,并使用它将landmark
对象传递给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)
}
}
步骤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)
}
}
步骤5
为最大(44毫米)和最小(38毫米)的表盘创建预览。
通过对最大和最小的手表表面进行测试,你可以看到你的应用程序在屏幕上的缩放效果。一如既往,您应该在所有支持的设备大小上测试用户界面。
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
中,并进行一些额外的布局更改,以便圆形图像适合任何手表的宽度。
步骤6
将圆图像嵌入到VStack
中。在图像下方显示地标名称及其信息。
如您所见,该信息不太适合在表盘屏幕上显示,但您可以通过将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")
}
}
}
步骤7
在滚动视图中包装VStack
。
这会打开视图滚动,但会产生另一个问题:圆形图像现在会扩展到原大小,并且会调整其他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")
}
}
}
步骤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")
}
}
}
步骤9
添加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)
}
.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")
}
}
}
步骤10
在后退
按钮上添加标题。
这会将后退按钮的文本设置为“地标”。但是,在添加“地标列表”视图之前,您不会看到“后退”按钮。
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")
}
}
}
第四节 添加watchOS地图视图
现在您已经创建了基本详情视图,现在是时候添加一个地图来显示地标的位置了。与CircleImage
不同,你不能重用iOS应用程序的MapView
。相反,您需要创建一个WKInterfaceObjectRepresentable
结构来包装WatchKit
的地图视图。
步骤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()
}
}
步骤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
协议属性。
步骤3
删除body()
方法并将其替换为landmark
属性。
无论何时创建地图视图,都需要传递此属性的值。例如,可以将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
实现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])
}
}
步骤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])
}
}
步骤6
选择WatchLandmarkView.swift
文件并将地图视图添加到VStack
的底部。
代码添加了一个分隔符,后跟地图视图。.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应用程序中的行Row
,但每个平台都需要呈现自己的详情视图。为了支持这一点,您将把LandmarkList视图转换为泛型列表类型,在这里实例化代码定义了详情视图。
Landmarks scheme
。
步骤1 在工具栏中,选择Xcode现在编译并运行应用程序的iOS版本。在将列表移动到watchOS应用程序之前,您要确保对LandmarkList
视图的任何修改在iOS应用程序中仍然有效。
步骤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(无法推断泛型参数)
错误。以下步骤会修复这些错误。
步骤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())
}
}
步骤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
实例时,还需要提供一个闭包,用于创建landmark
的详情视图。
步骤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())
}
}
步骤6
在LandmarkList.swift中,向预览添加类似的代码。
在这里,您需要使用条件判断,根据Xcode编译的当前scheme
定义详情视图。现在地标应用程序可以按照预期在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应用程序中。
步骤1
在文件检查器中,将LandmarksList.swift
添加到WatchLandmarks Extension
Target。
现在可以在watchOS应用程序的代码中使用LandmarkList视图。
步骤2
在工具栏中,将scheme
更改为WatchLandmarks
。
步骤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应用程序的根视图是ContentView,它显示默认文本
Hello World!
。
步骤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())
}
}
步骤5
在模拟器中编译运行watchOS应用程序。
通过滚动列表中的地标来测试watchOS应用程序的行为,点击以查看地标的详细信息,并将其标记为收藏夹。单击back
按钮返回列表,然后打开Favorite
开关,这样您只能看到收藏的地标。
第7节 创建自定义通知界面
你的watchOS版地标应用程序几乎完成了。在这最后一部分中,您将创建一个通知界面,每当您离最喜欢的某一位置很近时,会收到地标信息的通知信息。
注意:本节仅介绍如何在收到通知后显示通知。它不描述如何设置或发送通知。
步骤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)
}
}
步骤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.
}
}
步骤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.
}
}
步骤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.
}
}
步骤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收到通知时,它会创建与通知类别关联的通知控制器。若要设置通知控制器的类别,必须打开并编辑应用程序的情节提要。
步骤6
在项目导航栏中,选择“Watch Landmarks”文件夹并打开界面storyboard
。选择指向静态通知界面控制器的箭头。
步骤7
在属性检查器中,将Notification Category
的名称设置为LandmarkNear
。
配置测试负载来使用LandmarkNear类别,并传递通知控制器期望的数据。
步骤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
}
负载文件模拟远程通知中从服务器发送的数据。
步骤9
选择“Landmarks-Watch (Notification)” scheme,并编译和运行您的应用程序。
第一次运行通知Scheme时,系统将请求发送通知的权限。选择Allow(允许)
。模拟器随后显示一个可滚动的通知,其中包括:一个用于将地标应用程序标记为发送者的框、通知视图和通知操作的按钮。