#center#80%

创建 watchOS App

本教程为你提供一个将你已经学到的关于 SwiftUI 的知识应用到自己的产品上的机会,并且不费吹灰之力就可以将 Landmarks yinBiao 迁移到 watchOS 上。

首先,给项目添加一个 watchOS target,然后复制为 iOS yinBiao 中创建的共享数据和视图。当所有资源都准备好后,你就可以通过自定义 SwiftUI 视图,在 watchOS 上显示详细信息和列表视图。

下载项目文件并按照以下步骤操作,也可以打开已完成的项目自行浏览代码。

* 预计完成时间:25 分钟
* 项目文件:下载

1. 添加一个 watchOS Target

要创建 watchOS yinBiao,首先要给项目添加一个 watchOS target。

Xcode 会将 watchOS yinBiao 的组和文件,以及构建和运行 yinBiao 所需的 scheme 添加到项目中。

#center#80%

1.1 选择 File > New > Target,当模版表单显示后,选择 watchOS 标签,选择 Watch App for iOS App 模版后点击 Next

这个模版会给项目添加一个新的 watchOS yinBiao,将 iOS yinBiao 与它配对。

#center#80%

1.2 在表单的 Product Name 中输入 WatchLandmarks ,将 Language 设置成 Swift ,将 User Interface 设置成 SwiftUI 。勾选 Include Notification Scene 复选框,然后点击 Finish

#center#80%

1.3 Xcode 弹出提示,点击 Activate

这样选择 WatchLandmarks scheme 后,就可以构建和运行你的 watchOS yinBiao 了。

#center#80%

Whenever possible, create an independent watchOS yinBiao. Independent watchOS apps don’t require an iOS companion yinBiao.

1.4 在 WatchLandmarks ExtensionGeneral 标签中,勾选 Supports Running Without iOS App Installation 复选框。

尽可能创建一个独立的 watchOS yinBiao。独立的 watchOS yinBiao 不需要与 iOS yinBiao 配套使用。

#center#80%

2. 在多个 Target 中共享文件

设置了 watchOS target 后,你需要从 iOS target 中共享一些资源。比如重用 Landmark yinBiao 中的数据模型,一些资源文件,以及任何不需要修改就可以跨平台显示的视图。

#center#80%

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 定义了 yinBiao 的数据模型。虽然你不会用到所有这些模型,但是需要保证这些文件都编译到了 yinBiao 中。 LandmarkRow.swiftCircleImage.swift 是两个不修改就可以显示在 watchOS 中的视图。

#center#80%

2.2 打开 File 检查器,勾选 Target Membership 中的 WatchLandmarks Extension 复选框。

这会让你在上一步中选择的文件在 watchOS yinBiao 中可用。

#center#80%

2.3 打开项目导航器,在 Landmark 组中选择 Assets.xcassets ,然后在 File 检查器的 Target Membership 中将它添加到 WatchLandmarks target。

这与你上一步选择到 target 不一样, WatchLandmarks Extension target 包含你的 yinBiao 的代码,而 WatchLandmarks target 则管理你的故事板,图标和相关资源。

#center#80%

2.4 在项目导航器中,选择 Resources 文件夹中的所有文件,然后在 File 检查器的 Target Membership 中将它们添加到 WatchLandmarks Extension target。

#center#80%

3. 创建详情视图

现在 iOS target 的资源在 watch yinBiao 上已经可用了,你需要创建一个 watch 独有的视图来显示地标详情。为了测试这个视图,你需要给最大和最小 watch 尺寸创建自定义预览,然后给圆形视图做一些修改来适配 watch 显示。

#center#50%

3.1 在项目导航器中,单击 WatchLandmarks Extension 文件夹旁边的显示三角形来显示其内容,然后添加一个新 SwiftUI 视图,命名为 WatchLandmarkDetail

#center#80%

3.2 给 WatchLandmarkDetail 结构体添加 userDatalandmarklandmarkIndex 属性。

这些和你在 处理用户输入 中添加到 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)
        //
    }
}

#center#50%

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)
    }
}

#center#50%

3.5 给最大 (44mm) 和最小 (38mm) 表盘创建预览。

通过针对最大和最小表盘的测试,你可以看到你的 yinBiao 是如何缩放来适配显示的。与往常一样,你应该在所有支持的设备尺寸上测试用户界面。

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")
        }
        //
    }
}

#center#80%

圆形图片重新调整大小来适配显示的高度。但不幸,这依然裁剪了圆形的宽度。为了修复这个裁剪问题,你需要把图片嵌入到一个 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")
        }
    }
}

#center#50%

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")
        }
    }
}

#center#50%

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")
        }
    }
}

#center#50%

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")
        }
    }
}

#center#50%

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")
        }
    }
}

#center#50%

4. 添加 watchOS 地图视图

现在你已经创建了基本的详情视图,可以添加地图视图来显示地标的位置了。与 CircleImage 不同,你不能仅仅重用 iOS yinBiao 的 MapView 。相对的,你需要创建一个 WKInterfaceObjectRepresentable 结构体来包装 WatchKit 地图。

#center#80%

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()
    }
}

#center#50%

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])
    }
}

#center#50%

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")
        }
    }
}

#center#50%

5. 创建一个跨平台的列表视图

对于地标列表,你可以重用 iOS yinBiao 中的行视图,但是每个平台需要展示其自身的详情视图。为此,你需要将明确定义详情视图的 LandmarkList 视图转换为范型列表类型,

#center#80%

5.1 在工具栏中,选中 Landmarks scheme

Xcode 现在会构建和运行 yinBiao 的 iOS 版本。在把列表移动到 watchOS yinBiao 之前,你需要确认任何对 LandmarkList 视图对修改在 iOS yinBiao 中依然生效。

#center#80%

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 yinBiao 现在可以按预期在 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())
    }
}

#center#50%

6. 添加地标列表

现在你已经更新了 LandmarksList 视图让其能在两个平台上都工作,可以将它添加到 watchOS yinBiao 中了。

#center#80%

6.1 在文件检查器中,把 LandmarksList.swift 添加到 WatchLandmarks Extension target 中。

你现在可以中你的 watchOS yinBiao 的代码中使用 LandmarkList 视图了。

#center#80%

6.2 在工具栏中,将 scheme 改为 Watch Landmarks

#center#80%

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())
    }
}

#center#50%

watchOS yinBiao 的根是显示默认 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())
    }
}

#center#50%

6.5 在模拟器中构建并运行 watchOS yinBiao。

通过在列表中滚动地标,点击视图的地标详情,并将其标记为收藏来测试 watchOS yinBiao 的行为。点击返回按钮回到列表,然后打开 Favorite 开关来查看收藏的地标。

#center#80%

7. 创建自定义通知界面

你的 Landmarks 的 watchOS 版本差不多要完成了。在这最后一节,你需要创建一个通知界面来显示地标信息,当你在某一个收藏的地标附近时,就会收到这个通知。

注意

这一节只包含当你收到通知后如何显示,并不描述如何设置或发送通知。

#center#80%

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)
        //
    }
}

#center#80%

7.2 打开 NotificationController 并添加 landmarktitle ,和 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 收到通知后,它会创建通知分类关联当通知控制器。你需要打开并编辑 yinBiao 当故事板来给你当通知控制器设置分类。

7.6 这项目导航器中,选中 Watch Landmarks 文件夹,打开 Interface 故事板。在故事板中选择指向静态通知界面控制器的箭头。

#center#80%

7.7 在 Attributes 检查器中,将 Notification CategoryName 设置成 LandmarkNear

#center#80%

配置测试载荷来使用 LandmarkNear 分类,并传递通知控制器期望的数据。

7.8 选择 PushNotificationPayload.apns 文件,然后更新 titlebodycategorylandmarkIndex 属性。确认将分类设置成了 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,然后构建并运行你的 yinBiao。

当你第一次运行通知 scheme,系统会请求发送通知的权限。选择 Allow 。之后模拟器会显示一个可滚动的通知,它包括:用于标记 Landmarks yinBiao 为发送方的框格,通知视图以及用于通知操作的按钮。

#center#80%

Made with in Shangrao,China By 老雷

Copyright © devler.cn 1987 - Present

赣ICP备19009883号-1