SwiftUI官方教程解读

原文链接:https://www.jianshu.com/p/ecfdbea7a0ed

SwiftUI简介

SwiftUI是wwdc2019发布的一个新的UI框架,通过声明和修改视图来布局UI和创建流畅的动画效果。并且我们可以通过状态变量来进行数据绑定实现一次性布局;Xcode 11 内建了直观的新设计工具canvus,在整个开发过程中,预览可视化与代码可编辑性能同时支持并交互,让我们可以体验到代码和布局同步的乐趣;同时支持和UIkit的交互

设计工具canvus

  • 开发者可以在canvus中拖拽控件来构建界面, 所编辑的内容会立刻反应到代码上
  • 切换不同的视图文件时canvus会切换到不同的界面
  • 点击左下角的按钮钉我们可以把视图固定在活跃页面
  • 选中canvus中的控件command+click可以调出inspect布局控件的属性
  • 点击右上角的+可以获取新的控件并拖拽到对应的位置
  • 在live状态下我们可以在canvus中调试点击等可交互效果 但不能缩放视图大小
    每次修改或者增加属性需要点击resume刷新canvus
    landMarkDetail布局代码见布局部分

文件结构

创建一个SwiftUI文件,默认生成两个结构体。一个实现view的协议,在body属性里描述内容和布局;一个结构体声明预览的view 并进行初始化等信息,预览view是控制器的view时可以显示在多个模拟器设备,是控件view时可以设置frame,预览view是提供给canvus展示的,使用了#if DEBUG 指令,编译器会删除代码,不会随应用程序一起发布

#if DEBUG
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)
              //.previewLayout(.fixed(width: 300, height: 70)) 设置view控件大小
        }
        .environmentObject(UserData())
    }
}
#endif

布局

普通的view:将多个视图组合并嵌入到堆栈中,这些堆栈将视图水平、垂直或者前后组合在一起

VStack {  //这里的布局实现的是上图canvus中landMarkDetail的效果
            MapView(coordinate: landmark.locationCoordinate)
                .frame(height: 300)//不传width默认长度为整个界面
            CircleImage(image: landmark.image(forSize: 250))
                .offset(x: 0, y: -130)
                .padding(.bottom, -130)
            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(.subheadline)
                    Spacer() //将水平的两个控件撑开
                    Text(landmark.state)
                        .font(.subheadline)
                }
            }
            .padding()
            Spacer()
        }

列表的布局:要求数据是可被标识的
(1)唯一标识每个元素的主键路径

 List(landmarkData.identified(by: \.id)) { landmark in
            LandmarkRow(landmark: landmark)
        }

(2)数据类型实现Identifiable protocol,持有一个id 属性

struct Landmark: Hashable, Codable, Identifiable {
    var id: Int  //
    var name: String
    fileprivate var imageName: String
    fileprivate var coordinates: Coordinates
    var state: String
    var park: String
    var category: Category
}
  List(landmarkData) { landmark in
            LandmarkRow(landmark: landmark)
        }  //直接传数据源

导航

添加导航栏是将其嵌入到NavigationView中,点击跳转的控件包装在navigationButton中,以设置到目标视图的换位。navigationBarTitle设置导航栏的标题,navigationBarItems设置导航栏右边的item

  NavigationView {//显示导航view
            List {
                  //SwiftUI里面的类似switch的控件,可以在list中直接组合布局
                 Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                 }
                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                         //跳转到地标详细页面
                        NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))//导航标题
        }
    }

实现modal出一个view

  .navigationBarItems(trailing:
               //点击navigationBarItems modal出profileHost页面
                PresentationButton(
                    Image(systemName: "person.crop.circle")
                        .imageScale(.large)
                        .accessibility(label: Text("User Profile"))
                        .padding(),
                    destination: ProfileHost()
                )
            )

程序运行是从sceneDelegate定义的根视图开始的, UIhostingController 是UIViewController的子类

动画效果

SwiftUI包括带有预定义或自定义的基本动画 以及弹簧和流体动画,可以调整动画速度,设置延迟,重复动画等等
可以通过在一个动画修改器后面添加另一个动画修改器来关闭动画

  • 转场动画
    系统转场动画调用: hikeDetail(hike.hike).transition(.slide)
    自定义的转场动画:把转场动画作为AnyTransition类的类型属性 (方便点语法设置丰富自定义动画)
extension AnyTransition {
    static var moveAndFade: AnyTransition {
        let insertion = AnyTransition.move(edge: .trailing)
            .combined(with: .opacity)
        let removal = AnyTransition.scale()
            .combined(with: .opacity)
        return .asymmetric(insertion: insertion, removal: removal)
    }
}

HikeDetail(hike: hike).transition(.moveAndFade)调用转场动画;move(edge:)方法是让视图从同一边滑出来以及消失;asymmetric(insertion:removal:)设置出现和小时的不同的动画效果

  • 阻尼动画
var animation: Animation {  //定义成存储属性方便调用
        Animation.spring(initialVelocity: 5)//重力效果,值越大,弹性越大
            .speed(2)//动画时间,值越大动画速度越快
            .delay(0.03 * Double(index))
    }

基础动画

                Button(action: //点击按钮显示一个view带转场的动画效果
                    withAnimation {
                    	self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        //旋转90度
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        //.animation(nil) //关闭前面的旋转90度的动画效果,只显示下面的动画
                       //选中的时候放大为原来的1.5倍
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                      //  .animation(.basic()) 实现简单的基础动画
                        //.animation(.spring()) 阻尼动画
                    
                }

给图片按钮加动画效果, 对应的会有旋转和缩放会有动画;加到action时,即使点击完成后的显示没有给image的可做动画属性加动画效果,全部都有动画,包含旋转缩放和转场动画

数据流

利用SwiftUI环境中的存储 ,把自定义数据对象绑定到view ,SwiftUI监视到可绑对象任何影响视图的更改并在更改后显示正确的视图

  • 自定义绑定类型
    声明为绑定类型 BindableObject ,PassthroughSubject是Combine框架的消息发布者, SwiftUI通过这个消息发布者订阅对象,并在数据发生变化的时候更新任何需要刷新的视图
import Combine
import SwiftUI
final class UserData: BindableObject {
    let didChange = PassthroughSubject<UserData, Never>()
    
    var showFavoritesOnly = false {
        didSet {
            didChange.send(self)
        }
    }

    var landmarks = landmarkData {
        didSet {
            didChange.send(self)
        }
    }
}

当客户机需要更新数据的时候,可绑定对象通知其订阅者
eg:当其中一个属性发生更改时,在属性的didset里面通过didchange发布者发布更改

绑定属性
(1)state

@State var profile = Profile.default

状态是随时间变化影响页面布局内容和行为的值
给定类型的持久值,视图通过该持久值读取和监视该值。状态实例不是值本身;它是读取和修改值的一种方法。若要访问状态的基础值,请使用其值属性。
(2)binding

@Binding var profile: Profile//向子视图传递数据

(3)environmentObject :

@EnvironmentObject var userData: UserData

存储在当前环境中的数据,跨视图传递,在初始化持有对象的时候使用environmentObject(_:)赋值可以和前面的自定义绑定类型一起使用

let window = UIWindow(frame: UIScreen.main.bounds)
         window.rootViewController = UIHostingController(rootView: CategoryHome().environmentObject(UserData()))

绑定行为
是对可变状态或数据的引用,用$的前缀访问状态变量或者其属性之一实现绑定控件 也可以访问绑定属性来实现绑定

与UIkit的交互

表示UIkit的view和controller 需要创建遵UIViewRepresentable或者UIViewControllerRepresentable协议的结构体,SwiftUI管理他们的生命周期并在需要的时候更新
实现协议方法:

//创建展示的UIViewController,调用一次
func makeUIViewController(context: Self.Context) -> Self.UIViewControllerType
//将展示的UIViewController更新到最新的版本
 func updateUIViewController(_ uiViewController: Self.UIViewControllerType, context: Self.Context)
//创建协调器
 func makeCoordinator() -> Self.Coordinator

在结构体内嵌套定义一个coordinator类。SwiftUI管理coordinator并把它提供给context ,在makeUIView(context:)之前调用这个makeCoordinator()方法创建协调器,以便在配置视图控制器的时候可以访问coordinator对象
我们可以使用这个协调器来实现常见的Cocoa模式,例如委托、数据源和通过目标操作响应用户事件

这里以用UIPageViewController实现轮播图为例,要注意其中的更新页面的逻辑~

pageview作为主view,组合一个PageControl 和 PageViewController实现图片轮播效果
PageView:  @State var currentPage = 1 定义绑定属性 ,$currentPage实现绑定到PageViewController
PageViewController: @Binding var currentPage: Int 定义绑定属性,在更新的方法updateUIViewController里面绑定显示,点击pagecontrol的更新页面时pageviewcontroller可以更新到最新的页面
pagecontrol: @Binding var currentPage: Int定义绑定属性 ,updateUIView 绑定显示,pageview滑动更新页面 pagecontrol可以更新到正确的显示

struct PageView<Page: View>: View {
    var viewControllers: [UIHostingController<Page>]
    @State var currentPage = 1

    init(_ views: [Page]) {//传入的view用SwiftUI的controller包装好后面传给pagecontroller
        self.viewControllers = views.map { UIHostingController(rootView: $0) }
    }

    var body: some View {
        ZStack(alignment: .bottomTrailing) {//将currentpage绑定起来了
            PageViewController(controllers: viewControllers, currentPage: $currentPage)
            PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
                .padding()
             //Text("Current Page: \(currentPage)").padding(.trailing,30)
        }
    }
}
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
    var controllers: [UIViewController]
    @Binding var currentPage: Int

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator

        return pageViewController
    }
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
       //pageviewcontroller绑定currentpage显示当前的页面,pageView变化的时候,page更新页面
        pageViewController.setViewControllers(
            [controllers[currentPage]], direction: .forward, animated: true)

    }
    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PageViewController

        init(_ pageViewController: PageViewController) {
            self.parent = pageViewController
        }
      //左滑显示控制
        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController) -> UIViewController? {
            guard let index = parent.controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index == 0 {
                return parent.controllers.last
            }
            return parent.controllers[index - 1]
        }
       // 右滑动显示控制
        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let index = parent.controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index + 1 == parent.controllers.count {
                return parent.controllers.first
            }
            return parent.controllers[index + 1]
        }
        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            if completed,
                let visibleViewController = pageViewController.viewControllers?.first,
                let index = parent.controllers.firstIndex(of: visibleViewController) {
               //当view滑动停止的时候告诉pageview当前页面的index(数据变化 pageview更新pagecontrol的展示)
                parent.currentPage = index
            }
        }
    }
}
struct PageControl: UIViewRepresentable {
    var numberOfPages: Int
    @Binding var currentPage: Int

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UIPageControl {
        let control = UIPageControl()
        control.numberOfPages = numberOfPages
        control.addTarget(
            context.coordinator,
            action: #selector(Coordinator.updateCurrentPage(sender:)),
            for: .valueChanged)

        return control
    }

    func updateUIView(_ uiView: UIPageControl, context: Context) {
        uiView.currentPage = currentPage
    }

    class Coordinator: NSObject {
        var control: PageControl

        init(_ control: PageControl) {
            self.control = control
        }

        @objc
        func updateCurrentPage(sender: UIPageControl) {
            control.currentPage = sender.currentPage
        }
    }
}

KaTeX parse error: Invalid color: 'rgb(150,90,150)' at position 8: \color{r̲g̲b̲(̲1̲5̲0̲,̲9̲0̲,̲1̲5̲0̲)̲}{QA}: 当我们编辑一部分用户数据的时候,我们不希望在编辑数据完成的时候影响到其他的页面 那么我们需要创建一个副本数据, 当副本数据编辑完成的时候 用副本数据更新真正的数据, 使相关的页面变化 这部分的内容参见demo中profiles的部分;对于画图的部分demo中也有非常酷炫的示例,详情参见 HikeGraphBadge(徽章)

参考资料

Apple官网教程 :https://developer.apple.com/tutorials/swiftui/creating-and-combining-views
demo下载
SwiftUI documentation

作者简介

就职于甜橙金融(翼支付)信息技术部,负责 iOS 客户端开发
转载于 https://www.jianshu.com/p/ecfdbea7a0ed

[译] SwiftUI 官方教程 (十)

https://www.jianshu.com/p/17fc7929fcbb

2019.10.16 20:11:14字数 3,511阅读 311

由于 API 变动,此文章部分内容已失效,最新完整中文教程及代码请查看 https://github.com/WillieWangWei/SwiftUI-Tutorials

image

微信技术群

SwiftUI 代表未来构建 App 的方向,欢迎加群一起交流技术,解决问题。

加群现在需要申请了,可以先加我微信,备注 "SwiftUI",我会拉你进群。

创建 watchOS App

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

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

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

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

1. 添加一个 watchOS Target

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

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

image

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

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

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

image

1.3 Xcode 弹出提示,点击 Activate 。

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

image

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

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

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

image

2. 在多个 Target 中共享文件

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

image

2.1 在项目导航器中,按住 Command 键然后点击选中以下文件:LandmarkRow.swift , Landmark.swift , UserData.swift , Data.swift , Profile.swift , Hike.swift ,  CircleImage.swift

Landmark.swift , UserData.swift , Data.swift , Profile.swift , Hike.swift 定义了 app 的数据模型。虽然你不会用到所有这些模型,但是需要保证这些文件都编译到了 app 中。 LandmarkRow.swift 和 CircleImage.swift 是两个不修改就可以显示在 watchOS 中的视图。

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

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

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

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

image

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

3. 创建详情视图

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

image

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

image

3.2 给 WatchLandmarkDetail 结构体添加 userData , landmark 和 landmarkIndex 属性。

这些和你在 处理用户输入 中添加到 LandmarkDetail 结构体中的属性是一样的。

WatchLandmarkDetail.swift

import SwiftUI

struct WatchLandmarkDetail: View {
    //
    @EnvironmentObject var userData: UserData
    var landmark: Landmark
    
    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }
    //
    
    var body: some View {
        Text("Hello World!")
    }
}

struct WatchLandmarkDetail_Previews: PreviewProvider {
    static var previews: some View {
        WatchLandmarkDetail()
    }
}

在上一步添加属性后,你会在 Xcode 中得到一个缺少参数的错误。为了修复这个错误,你需要二选一:提供属性的默认值,或传递参数来设置视图的属性。

3.3 在预览中,创建一个用户数据的实例,然后用它给 WatchLandmarkView 结构体的初始化传递一个地标对象。另外还需要将这个用户数据设置成视图的环境对象。

WatchLandmarkDetail.swift

import SwiftUI

struct WatchLandmarkDetail: View {
    @EnvironmentObject var userData: UserData
    var landmark: Landmark
    
    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }
    
    var body: some View {
        Text("Hello World!")
    }
}

struct WatchLandmarkDetail_Previews: PreviewProvider {
    static var previews: some View {
        //
        let userData = UserData()
        return WatchLandmarkDetail(landmark: userData.landmarks[0])
            .environmentObject(userData)
        //
    }
}

image

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

image

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

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

WatchLandmarkDetail.swift

import SwiftUI

struct WatchLandmarkDetail: View {
    @EnvironmentObject var userData: UserData
    var landmark: Landmark
    
    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }
    
    var body: some View {
        CircleImage(image: self.landmark.image.resizable())
            .scaledToFill()
    }
}

struct WatchLandmarkDetail_Previews: PreviewProvider {
    static var previews: some View {
        let userData = UserData()
        //
        return Group {
            WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
                .previewDevice("Apple Watch Series 4 - 44mm")
            
            WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
                .previewDevice("Apple Watch Series 2 - 38mm")
        }
        //
    }
}

image

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

image

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

image

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

image

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

image

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

image

4. 添加 watchOS 地图视图

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

image

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

image

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

image

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

image

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

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

image

5.1 在工具栏中,选中 Landmarks scheme 。

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

5.2 选中 LandmarkList.swift 然后修改类型的声明将其变成范型类型。

LandmarksList.swift

import SwiftUI

//
struct LandmarkList<DetailView: View>: View {
//
    @EnvironmentObject private var userData: UserData
    
    var body: some View {
        List {
            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Show Favorites Only")
            }
            
            ForEach(userData.landmarks) { landmark in
                if !self.userData.showFavoritesOnly || landmark.isFavorite {
                    NavigationLink(
                    destination: LandmarkDetail(landmark: landmark).environmentObject(self.userData)) {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }
        }
        .navigationBarTitle(Text("Landmarks"))
    }
}

struct LandmarksList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
            LandmarkList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
                .previewDisplayName(deviceName)
        }
        .environmentObject(UserData())
    }
}

添加范型声明会让你无论何时创建一个 LandmarkList 结构体实例时都会出现 Generic parameter could not be inferred 错误。接下来的几步会修复这些错误。

5.3 添加一个创建详情视图的闭包属性。

LandmarksList.swift

import SwiftUI

struct LandmarkList<DetailView: View>: View {
    @EnvironmentObject private var userData: UserData
    
    //
    let detailViewProducer: (Landmark) -> DetailView
    //
    
    var body: some View {
        List {
            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Show Favorites Only")
            }
            
            ForEach(userData.landmarks) { landmark in
                if !self.userData.showFavoritesOnly || landmark.isFavorite {
                    NavigationLink(
                    destination: LandmarkDetail(landmark: landmark).environmentObject(self.userData)) {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }
        }
        .navigationBarTitle(Text("Landmarks"))
    }
}

struct LandmarksList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
            LandmarkList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
                .previewDisplayName(deviceName)
        }
        .environmentObject(UserData())
    }
}

5.4 使用 detailViewProducer 属性给地标创建详情视图。

LandmarksList.swift

import SwiftUI

struct LandmarkList<DetailView: View>: View {
    @EnvironmentObject private var userData: UserData
    
    let detailViewProducer: (Landmark) -> DetailView
    
    var body: some View {
        List {
            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Show Favorites Only")
            }
            
            ForEach(userData.landmarks) { landmark in
                if !self.userData.showFavoritesOnly || landmark.isFavorite {
                    NavigationLink(
                    //
                    destination: self.detailViewProducer(landmark).environmentObject(self.userData)) {
                    //
                        LandmarkRow(landmark: landmark)
                    }
                }
            }
        }
        .navigationBarTitle(Text("Landmarks"))
    }
}

struct LandmarksList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
            LandmarkList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
                .previewDisplayName(deviceName)
        }
        .environmentObject(UserData())
    }
}

当你创建了一个 LandmarkList 的实例后,你还需要提供一个给地标创建详情视图的闭包。

5.5 选中 Home.swift ,在 CategoryHome 结构体的 body() 方法中添加一个闭包来创建 LandmarkDetail 视图。

Xcode 会根据闭包的返回类型来推断 LandmarkList 结构体的类型。

Home.swift

import SwiftUI

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        Dictionary(
            grouping: landmarkData,
            by: { $0.category.rawValue }
        )
    }

    var featured: [Landmark] {
        landmarkData.filter { $0.isFeatured }
    }
    
    @State var showingProfile = false
    
    var profileButton: some View {
        Button(action: { self.showingProfile.toggle() }) {
            Image(systemName: "person.crop.circle")
                .imageScale(.large)
                .accessibility(label: Text("User Profile"))
                .padding()
        }
    }

    var body: some View {
        NavigationView {
            List {
                FeaturedLandmarks(landmarks: featured)
                    .scaledToFill()
                    .frame(height: CGFloat(200))
                    .clipped()
                    .listRowInsets(EdgeInsets())
                
                ForEach(categories.keys.sorted(), id: \.self) { key in
                    CategoryRow(categoryName: key, items: self.categories[key]!)
                }
                .listRowInsets(EdgeInsets())
                
                //
                NavigationLink(destination: LandmarkList { LandmarkDetail(landmark: $0) }) {
                //
                    Text("See All")
                }
            }
            .navigationBarTitle(Text("Featured"))
            .navigationBarItems(trailing: profileButton)
            .sheet(isPresented: $showingProfile) {
                ProfileHost()
            }
        }
    }
}

struct FeaturedLandmarks: View {
    var landmarks: [Landmark]
    var body: some View {
        landmarks[0].image.resizable()
    }
}

// swiftlint:disable type_name
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
            .environmentObject(UserData())
    }
}

5.6 在 LandmarkList.swift 中,给预览添加类似的代码。

在这里,你需要使用条件编译来根据 Xcode 的当前 scheme 来定义详细视图。Landmark app 现在可以按预期在 iOS 上构建并运行了。

LandmarksList.swift

import SwiftUI

struct LandmarkList<DetailView: View>: View {
    @EnvironmentObject private var userData: UserData
    
    let detailViewProducer: (Landmark) -> DetailView
    
    var body: some View {
        List {
            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Show Favorites Only")
            }
            
            ForEach(userData.landmarks) { landmark in
                if !self.userData.showFavoritesOnly || landmark.isFavorite {
                    NavigationLink(
                    destination: self.detailViewProducer(landmark).environmentObject(self.userData)) {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }
        }
        .navigationBarTitle(Text("Landmarks"))
    }
}

#if os(watchOS)
typealias PreviewDetailView = WatchLandmarkDetail
#else
typealias PreviewDetailView = LandmarkDetail
#endif

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList { PreviewDetailView(landmark: $0) }
            .environmentObject(UserData())
    }
}

6. 添加地标列表

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

image

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

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

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

6.3 打开 LandmarkList.swift 的同时,恢复预览。

现在预览会显示 watchOS 的列表视图。

LandmarksList.swift

import SwiftUI

struct LandmarkList<DetailView: View>: View {
    @EnvironmentObject private var userData: UserData
    
    let detailViewProducer: (Landmark) -> DetailView
    
    var body: some View {
        List {
            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Show Favorites Only")
            }
            
            ForEach(userData.landmarks) { landmark in
                if !self.userData.showFavoritesOnly || landmark.isFavorite {
                    NavigationLink(
                    destination: self.detailViewProducer(landmark).environmentObject(self.userData)) {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }
        }
        .navigationBarTitle(Text("Landmarks"))
    }
}

#if os(watchOS)
typealias PreviewDetailView = WatchLandmarkDetail
#else
typealias PreviewDetailView = LandmarkDetail
#endif

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList { PreviewDetailView(landmark: $0) }
            .environmentObject(UserData())
    }
}

image

watchOS app 的根是显示默认 Hello World! 消息的 ContentView 。

6.4 修改 ContentView 来让它显示列表视图。

ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        LandmarkList { WatchLandmarkDetail(landmark: $0) }
            .environmentObject(UserData())
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList { WatchLandmarkDetail(landmark: $0) }
            .environmentObject(UserData())
    }
}

image

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

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

7. 创建自定义通知界面

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

注意

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

image

7.1 打开 NotificationView.swift 并创建一个视图来显示地标的信息,标题和消息。

因为任何通知的值都可以为 nil,所以预览会显示通知的两个版本。第一个仅显示当没有数据时的默认值,第二个显示你提供的标题,信息,和位置。

NotificationView.swift

import SwiftUI

struct NotificationView: View {
    
    //
    let title: String?
    let message: String?
    let landmark: Landmark?
    
    init(title: String? = nil,
         message: String? = nil,
         landmark: Landmark? = nil) {
        self.title = title
        self.message = message
        self.landmark = landmark
    }
    //
    
    var body: some View {
        //
        VStack {
            
            if landmark != nil {
                CircleImage(image: landmark!.image.resizable())
                    .scaledToFit()
            }
            
            Text(title ?? "Unknown Landmark")
                .font(.headline)
                .lineLimit(0)
            
            Divider()
            
            Text(message ?? "You are within 5 miles of one of your favorite landmarks.")
                .font(.caption)
                .lineLimit(0)
        }
        //
    }
}

struct NotificationView_Previews: PreviewProvider {
    //
    //
    static var previews: some View {
        //
        Group {
            NotificationView()
            
            NotificationView(title: "Turtle Rock",
                             message: "You are within 5 miles of Turtle Rock.",
                             landmark: UserData().landmarks[0])
        }
        .previewLayout(.sizeThatFits)
        //
    }
}

7.2 打开 NotificationController 并添加 landmark , title ,和 message 属性。

这些数据存储发送进来的通知的相关值。

NotificationController.swift

import WatchKit
import SwiftUI
import UserNotifications

class NotificationController: WKUserNotificationHostingController<NotificationView> {
    //
    var landmark: Landmark?
    var title: String?
    var message: String?
    //
    
    override var body: NotificationView {
        NotificationView()
    }
    
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }
    
    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }
    
    override func didReceive(_ notification: UNNotification) {
        // This method is called when a notification needs to be presented.
        // Implement it if you use a dynamic notification interface.
        // Populate your dynamic notification interface as quickly as possible.
    }
}

7.3 更新 body() 方法来使用这些属性。

此方法会实例化你之前创建的通知视图。

NotificationController.swift

import WatchKit
import SwiftUI
import UserNotifications

class NotificationController: WKUserNotificationHostingController<NotificationView> {
    var landmark: Landmark?
    var title: String?
    var message: String?
    
    override var body: NotificationView {
        //
        NotificationView(title: title,
            message: message,
            landmark: landmark)
        //
    }
    
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }
    
    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }
    
    override func didReceive(_ notification: UNNotification) {
        // This method is called when a notification needs to be presented.
        // Implement it if you use a dynamic notification interface.
        // Populate your dynamic notification interface as quickly as possible.
    }
}

7.4 定义 LandmarkIndexKey 。

你需要使用这个键从通知中提取额地标的索引。

NotificationController.swift

import WatchKit
import SwiftUI
import UserNotifications

class NotificationController: WKUserNotificationHostingController<NotificationView> {
    var landmark: Landmark?
    var title: String?
    var message: String?
    
    //
    let landmarkIndexKey = "landmarkIndex"
    //
    
    override var body: NotificationView {
        NotificationView(title: title,
            message: message,
            landmark: landmark)
    }
    
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }
    
    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }
    
    override func didReceive(_ notification: UNNotification) {
        // This method is called when a notification needs to be presented.
        // Implement it if you use a dynamic notification interface.
        // Populate your dynamic notification interface as quickly as possible.
    }
}

7.5 更新 didReceive(_:) 方法从推送中解析数据。

这个方法会更新控制器的属性。调用这个方法后,系统会使控制器的 body 属性无效,从而更新导航视图。然后系统会在 Apple Watch 上显示通知。

NotificationController.swift

import WatchKit
import SwiftUI
import UserNotifications

class NotificationController: WKUserNotificationHostingController<NotificationView> {
    var landmark: Landmark?
    var title: String?
    var message: String?
    
    let landmarkIndexKey = "landmarkIndex"
    
    override var body: NotificationView {
        NotificationView(title: title,
            message: message,
            landmark: landmark)
    }
    
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }
    
    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }
    
    override func didReceive(_ notification: UNNotification) {
        //
        let userData = UserData()
        
        let notificationData =
            notification.request.content.userInfo as? [String: Any]
        
        let aps = notificationData?["aps"] as? [String: Any]
        let alert = aps?["alert"] as? [String: Any]
        
        title = alert?["title"] as? String
        message = alert?["body"] as? String
        
        if let index = notificationData?[landmarkIndexKey] as? Int {
            landmark = userData.landmarks[index]
        }
        //
    }
}

当 Apple Watch 收到通知后,它会创建通知分类关联当通知控制器。你需要打开并编辑 app 当故事板来给你当通知控制器设置分类。

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

image

7.7 在 Attributes 检查器中,将 Notification Category 的 Name 设置成 LandmarkNear 。

image

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

7.8 选择 PushNotificationPayload.apns 文件,然后更新 title , body , category 和 landmarkIndex 属性。确认将分类设置成了 LandmarkNear 。另外,删除在教程中任何没有用到的键,比如 subtitle , WatchKit Simulator Actions ,以及 customKey 。

载荷文件会模拟从服务发来的远程通知数据。

PushNotificationPayload.apns

{
    "aps": {
        "alert": {
            "body": "You are within 5 miles of Silver Salmon Creek."
            "title": "Silver Salmon Creek",
        },
        "category": "LandmarkNear",
        "thread-id": "5280"
    },
    
    "landmarkIndex": 1
}

7.9 选择 Landmarks-Watch (Notification) scheme,然后构建并运行你的 app。

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

image

发布了15 篇原创文章 · 获赞 3 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/sundaysme/article/details/103353766