02 Creating A watchOS App - ly918/SwiftUI-Chinese-Documents GitHub Wiki

框架集成

创建watchOS应用程序

本教程为您提供了一个机会,让您可以应用您已经了解的有关SwiftUI的大部分内容,并且只需很少的努力将Landmarks应用程序迁移到watchOS

在复制为iOS应用程序创建的共享数据和视图之前,您将首先向项目中添加watchOS target。所有资源就绪后,您将自定义SwiftUI视图,以便在watchOS上显示详情视图和列表视图。

学习时间:25分钟

下载地址:CreatingAwatchOSApp.zip

第一节 添加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

实现WKInterfaceObjectRepresentablemakeWKInterfaceObject(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

通过实现WKInterfaceObjectRepresentableupdateWKInterfaceObject(_:,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视图转换为泛型列表类型,在这里实例化代码定义了详情视图。

步骤1 在工具栏中,选择Landmarks scheme

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 ExtensionTarget。

现在可以在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并添加landmarktitlemessage属性。

这些数据存储了有关通知传入信息。

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。您还可以删除本教程中未使用的任何键,如subtitleWatchKit Simulator ActionscustomKey

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(允许)。模拟器随后显示一个可滚动的通知,其中包括:一个用于将地标应用程序标记为发送者的框、通知视图和通知操作的按钮。