#center#80%

创建 macOS App

在创建了一个 watchOS 版本的 Landmarks 之后,让我们把目光投向更大的内容:将 Landmarks 运行在 Mac 上。在你目前为止所学到的基础上,强化你在构建 iOS、watchOS 和 macOS 的 SwiftUI 应用的经验。

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

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

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

1. 添加一个 macOS Target

首先要给项目添加一个 macOS target。用 Xcode 给 macOS yinBiao 添加新的目录和一组初始文件,以及构建和运行该应用程序需要的 scheme。

#center#80%

1.1 选择 File > New > Target。出现模版选单后,选择 macOS 栏目,选中 App 模版然后点击 Next

这个模版会添加一个新的 macOS yinBiao target 到项目中。

#center#80%

1.2 在选单中,Product Name 输入 MacLandmarks 。把 Language 设置成 Swift ,把 User Interface 设置成 SwiftUI ,然后点击 Finish

#center#80%

1.3 将 scheme 设置成 MacLandmarks > My Mac

将 scheme 设置成 My Mac 之后,你就可以预览、构建和运行这个 macOS yinBiao 了。

#center#80%

接下来要构建的 yinBiao 依赖于低版本的 macOS 所不具备的某些功能,因此你需要更改 Deployment Target。

1.4 在项目导航栏中,选择顶部的 Xcode 项目,选择 target 下面的 MacLandmarks ,然后将 Deployment Target 设置成 10.15.3

#center#80%

1.5 在 MacLandmarks 目录中,选中 ContentView.swift ,打开 Canvas,然后点击 Resume 来观察预览。

和 iOS yinBiao 一样,SwiftUI 提供了默认的主视图及其 preview provider,让我们可以预览 yinBiao 的主窗口。

ContentView.swift
import SwiftUI

struct ContentView: View {

    var body: some View {
        Text("Hello, World!")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

#center#80%

2. 共享数据和资源

接下来,我们会从 iOS yinBiao 中重用模型和资源文件,并在 macOS target 中共享。

#center#50%

2.1 在项目导航中,打开 Landmarks 目录并选中 ModelsResources 中所有的文件。

landmarkData.json 文件包含在这个教程的初始项目中,它给每个 landmark 包含一个新的描述字段,这在以前的教程中是没有的。

#center#80%

2.2 在文件检查器中,将刚才选中文件的 Target Membership 设置成 MacLandmarks

在构建视图时,yinBiao 需要访问这些共享资源。

#center#80%

为了使用新的描述字段,我们需要给 Landmark 结构体添加一个新的对应字段。

2.3 打开 Landmark.swift 文件,添加一个新的描述属性。

由于基于 Codable 协议来加载数据,因此只需要确保属性名称与 JSON 中用于加载新数据的名称一致就可以了。

Landmark.swift
import SwiftUI
import CoreLocation

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
    var isFavorite: Bool
    var isFeatured: Bool
    var description: String //

    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(
            latitude: coordinates.latitude,
            longitude: coordinates.longitude)
    }
    
    var featureImage: Image? {
        guard isFeatured else { return nil }
        
        return Image(
            ImageStore.loadImage(name: "\(imageName)_feature"),
            scale: 2,
            label: Text(name))
    }

    enum Category: String, CaseIterable, Codable, Hashable {
        case featured = "Featured"
        case lakes = "Lakes"
        case rivers = "Rivers"
        case mountains = "Mountains"
    }
}

extension Landmark {
    var image: Image {
        ImageStore.shared.image(name: imageName)
    }
}

struct Coordinates: Hashable, Codable {
    var latitude: Double
    var longitude: Double
}

3. 创建行视图

使用 SwiftUI 的时候,通常从下至上构建视图。先创建较小的视图,然后将其组装为较大的视图。

首先,为 macOS 定义列表的单行的布局。该行包含 landmark 的名称、它的位置、一个图片以及表示这个 landmark 是否被收藏的可选标记。

#center#50%

3.1 给 MacLandmarks 目录添加一个新的 SwiftUI 视图,起名叫做 LandmarkRow.swift

这个视图和 iOS yinBiao 中一个的文件重名,但每个文件都有一个仅包含对应 yinBiao 的 target membership,这样就可以避免文件冲突。

LandmarkRow.swift
import SwiftUI

struct LandmarkRow: View {
    var body: some View {
       Text("Hello, World!")
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow()
    }
}

#center#50%

3.2 给 LandmarkRow 结构体添加一个 landmark 属性,然后给 preview provider 添加一个 landmark 用来显示。

LandmarkRow.swift
import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark //

    var body: some View {
       Text("Hello, World!")
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[0]) //
    }
}

3.3 用以一个水平 stack 来替换掉占位符,它用来绘制 landmark 的图片。

LandmarkRow.swift
import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        //
        HStack(alignment: .center) {
            landmark.image
                .resizable()
                .aspectRatio(1.0, contentMode: .fit)
                .frame(width: 32, height: 32)
                .fixedSize(horizontal: true, vertical: false)
                .cornerRadius(4.0)
        }
        //
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[0])
    }
}

#center#50%

3.4 添加关于 landmark 的文字,然后组合到一个竖直 stack 中。

LandmarkRow.swift
import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack(alignment: .center) {
            landmark.image
                .resizable()
                .aspectRatio(1.0, contentMode: .fit)
                .frame(width: 32, height: 32)
                .fixedSize(horizontal: true, vertical: false)
                .cornerRadius(4.0)

            //
            VStack(alignment: .leading) {
                Text(landmark.name)
                    .fontWeight(.bold)
                    .truncationMode(.tail)
                    .frame(minWidth: 20)

                Text(landmark.park)
                    .font(.caption)
                    .opacity(0.625)
                    .truncationMode(.middle)
            }
            //
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[0])
    }
}

#center#50%

3.5 给视图添加一个收藏指示器,然后通过一个 spacer 和已有的内容隔开。

spacer 可以将已有的内容推到左边,但是需要出现在右边的指示器现在还看不到,因为我们还没有把对应的图片资源添加到 yinBiao 中。

LandmarkRow.swift
import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack(alignment: .center) {
            landmark.image
                .resizable()
                .aspectRatio(1.0, contentMode: .fit)
                .frame(width: 32, height: 32)
                .fixedSize(horizontal: true, vertical: false)
                .cornerRadius(4.0)

            VStack(alignment: .leading) {
                Text(landmark.name)
                    .fontWeight(.bold)
                    .truncationMode(.tail)
                    .frame(minWidth: 20)

                Text(landmark.park)
                    .font(.caption)
                    .opacity(0.625)
                    .truncationMode(.middle)
            }

            //
            Spacer()

            if landmark.isFavorite {
               Image("star-filled")
                   .resizable()
                   .renderingMode(.template)
                   .foregroundColor(.yellow)
                   .frame(width: 10, height: 10)
            }
            //
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[0])
    }
}

#center#50%

3.6 在下载的项目资源目录中,把 star-filled.pdfstar-empty.pdf 文件拖拽到 Mac yinBiao 的资源文件夹中。

#center#80%

3.7 给行视图的内容添加一个竖直 padding,用来显示一个星星并填充黄色来表示收藏。

之后将多个行视图放在一个列表中时,padding 能提高可读性。

LandmarkRow.swift
import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack(alignment: .center) {
            landmark.image
                .resizable()
                .aspectRatio(1.0, contentMode: .fit)
                .frame(width: 32, height: 32)
                .fixedSize(horizontal: true, vertical: false)
                .cornerRadius(4.0)

            VStack(alignment: .leading) {
                Text(landmark.name)
                    .fontWeight(.bold)
                    .truncationMode(.tail)
                    .frame(minWidth: 20)

                Text(landmark.park)
                    .font(.caption)
                    .opacity(0.625)
                    .truncationMode(.middle)
            }

            Spacer()

            if landmark.isFavorite {
               Image("star-filled")
                   .resizable()
                   .renderingMode(.template)
                   .foregroundColor(.yellow)
                   .frame(width: 10, height: 10)
            }
        }
        .padding(.vertical, 4) //
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[0])
    }
}

#center#50%

4. 将行视图组合到列表中

通过使用上一步定义的行视图,我们可以创建一个列表来给用户展示所有的已知 landmark。当 UserData 中的 showFavoritesOnly 属性为 true 时,我们限制只显示被收藏的 landmark。

#center#50%

4.1 在构建中添加一个新的 SwiftUI 视图,起名为 LandmarkList.swift

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {

    var body: some View {
        Text("Hello, World!")
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}

#center#80%

4.2 添加一个 userData 属性作为 environment object,然后更新 preview provider。

这样视图就可以访问描述 landmark 的全局 UserData 了。

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject private var userData: UserData //

    var body: some View {
        Text("Hello, World!")
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
            .environmentObject(UserData()) //
    }
}

4.3 创建一个列表,用来持有我们在 LandmarkRow 中创建的行视图。

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject private var userData: UserData

    var body: some View {
        //
        List {
            ForEach(userData.landmarks) { landmark in
                LandmarkRow(landmark: landmark)
            }
        }
        //
    }
}

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

#center#80%

4.4 为了让行视图可选中,我们需要为列表提供一个可选的选中 landmark 的 binding,并用 landmark 来标记它。

之后,我们会使用选中的 landmark 来驱动详细视图的内容。

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject private var userData: UserData
    @Binding var selectedLandmark: Landmark? //

    var body: some View {
        List(selection: $selectedLandmark) { //
            ForEach(userData.landmarks) { landmark in
                LandmarkRow(landmark: landmark).tag(landmark) //
            }
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList(selectedLandmark: .constant(landmarkData[0])) //
            .environmentObject(UserData())
    }
}

#center#80%

4.5 根据 showFavoritesOnly 属性的状态,以及收藏 landmark 的状态的组合来限制行视图的创建。

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject private var userData: UserData
    @Binding var selectedLandmark: Landmark?

    var body: some View {
        List(selection: $selectedLandmark) {
            ForEach(userData.landmarks) { landmark in
                //
                if (!self.userData.showFavoritesOnly || landmark.isFavorite) {
                    LandmarkRow(landmark: landmark).tag(landmark)
                }
                //
            }
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList(selectedLandmark: .constant(landmarkData[0])) 
            .environmentObject(UserData())
    }
}

5. 创建一个过滤器来管理列表

因为用户可以将一个 landmark 标记为收藏,所以我们需要提供一个方法只显示它们收藏的 landmark。创建一个过滤视图,它使用一个 Toggle 为用户提供一个复选框,用户可以单击该复选框打开或关闭过滤。

为了让用户快速缩小自己收藏的 landmark 列表的范围,我们会添加一个 Picker 来创建一个弹出按钮,用户可以根据自己设置的任何分类来过滤自己的收藏。

#center#80%

5.1 给构建添加一个新的 SwiftUI 视图,起名 Filter.swift

Filter.swift
import SwiftUI

struct Filter: View {

    var body: some View {
        Text("Hello, World!")
    }
}

struct Filter_Previews: PreviewProvider {
    static var previews: some View {
        Filter()
    }
}

#center#50%

5.2 添加一个 userData 属性作为 environment object,然后更新 preview provider。

Filter.swift
import SwiftUI


struct Filter: View {
    @EnvironmentObject private var userData: UserData //

    var body: some View {
        Text("Hello, World!")
    }
}

struct Filter_Previews: PreviewProvider {
    static var previews: some View {
        Filter()
            .environmentObject(UserData()) //
    }
}

5.3 将默认的文本替换成绑定到 showFavoritesOnly 布尔值的 toggle,并为其指定适当的 label。

当用户修改这个 toggle,列表视图会自动刷新。因为它绑定到了环境中的相同的 showFavoritesOnly 值。

Filter.swift
import SwiftUI

struct Filter: View {
    @EnvironmentObject private var userData: UserData

    var body: some View {
        //
        HStack {
            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Favorites only")
            }
       }
       //
    }
}

struct Filter_Previews: PreviewProvider {
    static var previews: some View {
        Filter()
            .environmentObject(UserData())
    }
}

#center#50%

我们可以使用 landmark 分类信息来定义其他过滤。

5.4 创建一个 FilterType 来持有一个 landmark 的分类和对应的名字。

保证它符合 Hashable 协议,我们就可以将这个 FilterType 作为一个 picker 的选项,同时用名字作为选项的说明。

Filter.swift
import SwiftUI

struct Filter: View {
    @EnvironmentObject private var userData: UserData

    var body: some View {
        HStack {
            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Favorites only")
            }
       }
    }
}

struct Filter_Previews: PreviewProvider {
    static var previews: some View {
        Filter()
            .environmentObject(UserData())
    }
}

//
struct FilterType: Hashable {
    var name: String
    var category: Landmark.Category?

    init(_ category: Landmark.Category) {
        self.name = category.rawValue
        self.category = category
    }
}
//

5.5 定义一个全部类型来表示不需要过滤。

这个额外类型需要一个新的初始化方法来处理 nil 类别的特殊情况。

Filter.swift
import SwiftUI

struct Filter: View {
    @EnvironmentObject private var userData: UserData

    var body: some View {
        HStack {
            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Favorites only")
            }
       }
    }
}

struct Filter_Previews: PreviewProvider {
    static var previews: some View {
        Filter()
            .environmentObject(UserData())
    }
}

struct FilterType: Hashable {
    var name: String
    var category: Landmark.Category?

    init(_ category: Landmark.Category) {
        self.name = category.rawValue
        self.category = category
    }

    //
    init(name: String) {
        self.name = name
        self.category = nil
    }

    static var all = FilterType(name: "All")
    //
}

遵循 CaseIterableIdentifiable 协议可以将 FilterType 结构体作为 ForEach 初始化方法中的数据,这样我们就可以将它添加到后续两步中。

5.6 实现 CaseIterable 协议,提供一个列表来表示所有可能的情况。

Filter.swift
import SwiftUI

struct Filter: View {
    @EnvironmentObject private var userData: UserData

    var body: some View {
        HStack {
            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Favorites only")
            }
       }
    }
}

struct Filter_Previews: PreviewProvider {
    static var previews: some View {
        Filter()
            .environmentObject(UserData())
    }
}

struct FilterType: CaseIterable, Hashable { //
    var name: String
    var category: Landmark.Category?

    init(_ category: Landmark.Category) {
        self.name = category.rawValue
        self.category = category
    }

    init(name: String) {
        self.name = name
        self.category = nil
    }

    static var all = FilterType(name: "All")

    //
    static var allCases: [FilterType] {
        return [.all] + Landmark.Category.allCases.map(FilterType.init)
    }
    //
}

5.7 实现 Identifiable 协议,定义 id 属性。

Filter.swift
import SwiftUI

struct Filter: View {
    @EnvironmentObject private var userData: UserData

    var body: some View {
        HStack {
            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Favorites only")
            }
       }
    }
}

struct Filter_Previews: PreviewProvider {
    static var previews: some View {
        Filter()
            .environmentObject(UserData())
    }
}

struct FilterType: CaseIterable, Hashable, Identifiable { //
    var name: String
    var category: Landmark.Category?

    init(_ category: Landmark.Category) {
        self.name = category.rawValue
        self.category = category
    }

    init(name: String) {
        self.name = name
        self.category = nil
    }

    static var all = FilterType(name: "All")

    static var allCases: [FilterType] {
        return [.all] + Landmark.Category.allCases.map(FilterType.init)
    }

    //
    var id: FilterType {
        return self
    }
    //
}

5.8 在过滤视图中,添加一个 picker,它使用 FilterType 实例的 binding 作为选项,使用 FilterType 的名字作为菜单的选择。

FilterType 实例使用 binding,可以让该视图的父视图观察用户的选择。

Filter.swift
import SwiftUI

struct Filter: View {
    @EnvironmentObject private var userData: UserData
    @Binding var filter: FilterType //

    var body: some View {
        HStack {
            //
            Picker(selection: $filter, label: EmptyView()) {
               ForEach(FilterType.allCases) { choice in
                   Text(choice.name).tag(choice)
               }
            }

            Spacer()
            //

            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Favorites only")
            }
       }
    }
}

struct Filter_Previews: PreviewProvider {
    static var previews: some View {
        Filter(filter: .constant(.all)) //
            .environmentObject(UserData())
    }
}

struct FilterType: CaseIterable, Hashable, Identifiable {
    var name: String
    var category: Landmark.Category?

    init(_ category: Landmark.Category) {
        self.name = category.rawValue
        self.category = category
    }

    init(name: String) {
        self.name = name
        self.category = nil
    }

    static var all = FilterType(name: "All")

    static var allCases: [FilterType] {
        return [.all] + Landmark.Category.allCases.map(FilterType.init)
    }

    var id: FilterType {
        return self
    }
}

#center#50%

5.9 返回上一节中的列表视图,添加一个 FilterType 的 binding。

与过滤器视图一样,将会与父视图共享。

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject private var userData: UserData
    @Binding var selectedLandmark: Landmark?
    @Binding var filter: FilterType //

    var body: some View {
        List(selection: $selectedLandmark) {
            ForEach(userData.landmarks) { landmark in
                if (!self.userData.showFavoritesOnly || landmark.isFavorite) {
                    LandmarkRow(landmark: landmark).tag(landmark)
                }
            }
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        //
        LandmarkList(selectedLandmark: .constant(landmarkData[0]),
                     filter: .constant(.all))
        //
            .environmentObject(UserData())
    }
}

5.10 更新限制创建行视图的逻辑,加入分类的过滤。

查找 landmark 的分类来匹配选中的分类,或在用户选择特色分类时查找任何特色地标。

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject private var userData: UserData
    @Binding var selectedLandmark: Landmark?
    @Binding var filter: FilterType

    var body: some View {
        List(selection: $selectedLandmark) {
            ForEach(userData.landmarks) { landmark in
                //
                if (!self.userData.showFavoritesOnly || landmark.isFavorite)
                    && (self.filter == .all
                        || self.filter.category == landmark.category
                        || (self.filter.category == .featured && landmark.isFeatured)) {
                //
                    LandmarkRow(landmark: landmark).tag(landmark)
                }
            }
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList(selectedLandmark: .constant(landmarkData[0]),
                     filter: .constant(.all))
            .environmentObject(UserData())
    }
}

6. 组合列表和过滤视图

创建一个组合了过过滤器和列表的主视图。将 landmark 选项绑定到主视图的父视图的同时,为过滤器提供新的状态信息。

#center#50%

6.1 在项目中创建一个新的 SwiftUI 视图,起名 NavigationMaster.swift

NavigationMaster.swift
import SwiftUI

struct NavigationMaster: View {

    var body: some View {
        Text("Hello, World!")
    }
}

struct NavigationMaster_Previews: PreviewProvider {
    static var previews: some View {
        NavigationMaster()
    }
}

#center#80%

6.2 声明过滤器的状态。

在这添加状态会使此视图成为该信息的真相来源。在接下来的几步中,我们会将此属性绑定到过滤器视图和列表视图。

NavigationMaster.swift
import SwiftUI

struct NavigationMaster: View {
    @State private var filter: FilterType = .all //

    var body: some View {
        Text("Hello, World!")
    }
}

struct NavigationMaster_Previews: PreviewProvider {
    static var previews: some View {
        NavigationMaster()
    }
}

6.3 添加过滤视图,并将它绑定到过滤器状态上。

此时 preview 会构建失败。因为过滤器依赖环境中的用户信息对象,我们会在下一步修复这个问题。

NavigationMaster.swift
import SwiftUI

struct NavigationMaster: View {
    @State private var filter: FilterType = .all

    var body: some View {
        
        //
        VStack {
            Filter(filter: $filter)
                .controlSize(.small)
                .padding([.top, .leading], 8)
                .padding(.trailing, 4)
        }
        //
    }
}

struct NavigationMaster_Previews: PreviewProvider {
    static var previews: some View {
        NavigationMaster()
    }
}

6.4 在环境中添加 UserData 对象。

尽管导航主视图不需要直接的 UserData ,但子视图却需要。要启用 preview,需要请将 UserData 作为环境对象提供给导航主视图。

NavigationMaster.swift
import SwiftUI

struct NavigationMaster: View {
    @State private var filter: FilterType = .all

    var body: some View {
        VStack {
            Filter(filter: $filter)
                .controlSize(.small)
                .padding([.top, .leading], 8)
                .padding(.trailing, 4)
        }
    }
}

struct NavigationMaster_Previews: PreviewProvider {
    static var previews: some View {
        NavigationMaster()
            .environmentObject(UserData()) //
    }
}

#center#50%

6.5 给选中的 landmark 添加一个 binding。

NavigationMaster.swift
import SwiftUI

struct NavigationMaster: View {
    @Binding var selectedLandmark: Landmark? //
    @State private var filter: FilterType = .all

    var body: some View {
       VStack {
          Filter(filter: $filter)
              .controlSize(.small)
              .padding([.top, .leading], 8)
              .padding(.trailing, 4)
        }
    }
}

struct NavigationMaster_Previews: PreviewProvider {
    static var previews: some View {
        NavigationMaster(selectedLandmark: .constant(landmarkData[1])) //
            .environmentObject(UserData())
    }
}

6.6 添加 landmark 列表视图,将它绑定到选中的 landmark 和过滤器的状态上。

preview 选择了列表中的第二项,因为我们提供了 landmarkData[1] 作为 selectedLandmark 的输入。

NavigationMaster.swift
import SwiftUI

struct NavigationMaster: View {
    @Binding var selectedLandmark: Landmark?
    @State private var filter: FilterType = .all

    var body: some View {
       VStack {
          Filter(filter: $filter)
              .controlSize(.small)
              .padding([.top, .leading], 8)
              .padding(.trailing, 4)
           
           //
           LandmarkList(
               selectedLandmark: $selectedLandmark,
               filter: $filter
           )
           .listStyle(SidebarListStyle())
           //
        }
    }
}

struct NavigationMaster_Previews: PreviewProvider {
    static var previews: some View {
        NavigationMaster(selectedLandmark: .constant(landmarkData[1]))
            .environmentObject(UserData())
    }
}

#center#80%

6.7 约束导航视图的宽度,防止用户让它过宽或过窄。

NavigationMaster.swift
import SwiftUI

struct NavigationMaster: View {
    @Binding var selectedLandmark: Landmark?
    @State private var filter: FilterType = .all

    var body: some View {
       VStack {
          Filter(filter: $filter)
              .controlSize(.small)
              .padding([.top, .leading], 8)
              .padding(.trailing, 4)
           
           LandmarkList(
               selectedLandmark: $selectedLandmark,
               filter: $filter
           )
           .listStyle(SidebarListStyle())
        }
        .frame(minWidth: 225, maxWidth: 300) //
    }
}

struct NavigationMaster_Previews: PreviewProvider {
    static var previews: some View {
        NavigationMaster(selectedLandmark: .constant(landmarkData[1]))
            .environmentObject(UserData())
    }
}

#center#80%

7. 准备重用的 CircleImage

有时我们仅需进行少量修改就能跨平台共享视图。在为 macOS 构建 landmark 详细视图时,我们会重用为 iOS 创建的 CircleImage 。为了满足 macOS 的不同布局要求,我们会添加一个参数来控制阴影半径。

#center#50%

7.1 在项目导航中,选择 Landmarks > Supporting Views ,然后选中 CircleImage.swift 文件。

#center#80%

7.2 把 CircleImage.swift 文件添加到 MacLandmarks target 中。

#center#80%

7.3 中 CircleImage.swift 中,修改结构体,添加一个新的阴影半径参数。

通过给新参数提供与以前的常量相同的默认值,可以确保 CircleImage 在现有客户端(如 iOS 和 watchOS yinBiao)在不做任何修改的情况下仍能像以前一样运行。

CircleImage.swift
import SwiftUI

struct CircleImage: View {
    var image: Image
    var shadowRadius: CGFloat = 10 //

    var body: some View {
        image
            .clipShape(Circle())
            .overlay(Circle().stroke(Color.white, lineWidth: 4))
            .shadow(radius: shadowRadius) //
    }
}

struct CircleImage_Previews: PreviewProvider {
    static var previews: some View {
        CircleImage(image: Image("turtlerock"))
    }
}

8 在 macOS 上展开 Map View

和圆形视图一样,我们会在 macOS 上重用 MapView 。但是, MapView 需要更大量的更新,因为它对 MapKit 的使用依赖于集成 UIKit 框架,在 macOS 中使用 MapKit 则需要集成 AppKit 框架。因此我们会添加一个编译时指令,为给定 target 提供正确的集成。

#center#50%

8.1 在项目导航中,选择 Landmarks > Supporting Views ,然后选中 MapView.swift 文件。

#center#80%

8.2 将 MapView.swift 文件添加到 MacLandmarks target 中。

此时 Xcode 会报错,因为地图视图使用了 UIViewRepresentable ,但它在 macOS SDK 中不支持。在下面但几步中,我们会在合适的时候使用 NSViewRepresentable 来展开这个视图。

#center#80%

8.3 插入可创建平台特定行为区域的编译指令。

我们会使用编译指令的两个分支来区分 UIViewRepresentableNSViewRepresentable 协议。

MapView.swift
import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    var coordinate: CLLocationCoordinate2D

    func makeUIView(context: Context) -> MKMapView {
        MKMapView(frame: .zero)
    }

    func updateUIView(_ view: MKMapView, context: Context) {
        let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        view.setRegion(region, animated: true)
    }
}

//
#if os(macOS)

#else

#endif
//

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView(coordinate: landmarkData[0].locationCoordinate)
    }
}

8.4 将由 makeUIViewupdateUIView 方法组成的 UIViewRepresentable 协议移动到适当的编译指令分支中的扩展中,这样就不用修改 MapKit 的实际交互。

此时 Xcode 仍报告使用未声明类型 Context 的错误。我们会在下一步中添加 NSViewRepresentable 协议来解决这个问题。

MapView.swift
import SwiftUI
import MapKit

struct MapView { //
    var coordinate: CLLocationCoordinate2D

    func makeMapView() -> MKMapView { //
        MKMapView(frame: .zero)
    }

    func updateMapView(_ view: MKMapView, context: Context) { //
        let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        view.setRegion(region, animated: true)
    }
}

#if os(macOS)

#else
//
extension MapView: UIViewRepresentable {
    func makeUIView(context: Context) -> MKMapView {
        makeMapView()
    }
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        updateMapView(uiView, context: context)
    }
}
//
#endif

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView(coordinate: landmarkData[0].locationCoordinate)
    }
}

8.5 添加与 UIViewRepresentable 对应的 NSViewRepresentable

UIViewRepresentable 一样, NSViewRepresentable 依赖于完成上一步后剩下的通用功能。

MapView.swift
import SwiftUI
import MapKit

struct MapView {
    var coordinate: CLLocationCoordinate2D

    func makeMapView() -> MKMapView {
        MKMapView(frame: .zero)
    }

    func updateMapView(_ view: MKMapView, context: Context) {
        let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        view.setRegion(region, animated: true)
    }
}

#if os(macOS)
//
extension MapView: NSViewRepresentable {
    func makeNSView(context: Context) -> MKMapView {
        makeMapView()
    }
    
    func updateNSView(_ nsView: MKMapView, context: Context) {
        updateMapView(nsView, context: context)
    }
}
//
#else

extension MapView: UIViewRepresentable {
    func makeUIView(context: Context) -> MKMapView {
        makeMapView()
    }
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        updateMapView(uiView, context: context)
    }
}

#endif

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView(coordinate: landmarkData[0].locationCoordinate)
    }
}

#center#50%

9. 构建详情视图

详情视图显示选中 landmark 的相关信息。我们会创建一个想 iOS yinBiao 一样的视图,但是不同平台有不同的数据展示方式。

我们会裁剪详情视图来适配 macOS,并且重用一些前两节准备的视图。

#center#50%

9.1 给项目添加一个新的视图,叫做 NavigationDetail.swift ,并给它添加一个 landmark 属性。

实例化详情视图的视图会使用此属性指明要显示的 landmark。

NavigationDetail.swift
import SwiftUI

struct NavigationDetail: View {
    var landmark: Landmark //

    var body: some View {
        Text("Hello, World!")
    }
}

struct NavigationDetail_Previews: PreviewProvider {
    static var previews: some View {
        NavigationDetail(landmark: landmarkData[0]) //
    }
}

#center#50%

9.2 创建一个包含竖直 stack 的滚动视图,同时又包含一个水平 stack,用于显示 CircleImage 和有关 landmark 的文本。

通过设置垂直 stack 的最大宽度,可以确保其所有内容的宽度保持在合适的阅读范围内。

NavigationDetail.swift
import SwiftUI

struct NavigationDetail: View {
    var landmark: Landmark

    var body: some View {
        //
        ScrollView {
            VStack(alignment: .leading, spacing: 12) {
                HStack(alignment: .center, spacing: 24) {
                    CircleImage(image: landmark.image)
                    
                    VStack(alignment: .leading) {
                        Text(landmark.name).font(.title)
                        Text(landmark.park)
                        Text(landmark.state)
                    }
                    .font(.caption)
                }
            }
            .padding()
            .frame(maxWidth: 700)
        }
        //
    }
}

struct NavigationDetail_Previews: PreviewProvider {
    static var previews: some View {
        NavigationDetail(landmark: landmarkData[0])
    }
}

#center#50%

尽管跨平台重用视图很方便,但我们仍需要自定义 CircleImage 视图来适配此布局。

9.3 通过让输入的图像可调整大小,并约束视图的 frame,我们可以减小 CircleImage 的大小来匹配关联的文本块。

在这之后,我们就不再需要修改相关的 CircleImage 来。

NavigationDetail.swift
import SwiftUI

struct NavigationDetail: View {
    var landmark: Landmark

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 12) {
                HStack(alignment: .center, spacing: 24) {
                    //
                    CircleImage(image: landmark.image.resizable())
                        .frame(width: 160, height: 160)
                    //
                    
                    VStack(alignment: .leading) {
                        Text(landmark.name).font(.title)
                        Text(landmark.park)
                        Text(landmark.state)
                    }
                    .font(.caption)
                }
            }
            .padding()
            .frame(maxWidth: 700)
        }
    }
}

struct NavigationDetail_Previews: PreviewProvider {
    static var previews: some View {
        NavigationDetail(landmark: landmarkData[0])
    }
}

#center#50%

9.4 调整阴影半径来适配更小的图片。

此修改通过 CircleImage 添加到项目时引入的参数来设置。

NavigationDetail.swift
import SwiftUI

struct NavigationDetail: View {
    var landmark: Landmark

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 12) {
                HStack(alignment: .center, spacing: 24) {
                    CircleImage(image: landmark.image.resizable(), shadowRadius: 4) //
                        .frame(width: 160, height: 160)
                    
                    VStack(alignment: .leading) {
                        Text(landmark.name).font(.title)
                        Text(landmark.park)
                        Text(landmark.state)
                    }
                    .font(.caption)
                }
            }
            .padding()
            .frame(maxWidth: 700)
        }
    }
}

struct NavigationDetail_Previews: PreviewProvider {
    static var previews: some View {
        NavigationDetail(landmark: landmarkData[0])
    }
}

#center#50%

我们可以使用按钮来控制用户是否将 landmark 标记为收藏。如果要修改,必须访问存储在 UserData 对象中的单个实质来源。

9.5 添加 UserData 作为一个环境对象,并基于当前选择的 landmark 在存储的 landmark 中创建索引。

NavigationDetail.swift
import SwiftUI

struct NavigationDetail: 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(alignment: .leading, spacing: 12) {
                HStack(alignment: .center, spacing: 24) {
                    CircleImage(image: landmark.image.resizable(), shadowRadius: 4)
                        .frame(width: 160, height: 160)
                    
                    VStack(alignment: .leading) {
                        Text(landmark.name).font(.title)
                        Text(landmark.park)
                        Text(landmark.state)
                    }
                    .font(.caption)
                }
            }
            .padding()
            .frame(maxWidth: 700)
        }
    }
}

struct NavigationDetail_Previews: PreviewProvider {
    static var previews: some View {
        NavigationDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}

9.6 添加一个按钮,与 landmark 名称水平对齐。使用行视图中的同一星形图像,它可以切换 landmark 的 isFavorite 属性。

对 landmark 进行更改后,需要在 UserData 中查找 landmark,并将所做的更改持久保存在数据存储中。

NavigationDetail.swift
import SwiftUI

struct NavigationDetail: 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(alignment: .leading, spacing: 12) {
                HStack(alignment: .center, spacing: 24) {
                    CircleImage(image: landmark.image.resizable(), shadowRadius: 4)
                        .frame(width: 160, height: 160)
                    
                    VStack(alignment: .leading) {
                        //
                        HStack {
                            Text(landmark.name).font(.title)
                            
                            Button(action: {
                                self.userData.landmarks[self.landmarkIndex]
                                    .isFavorite.toggle()
                            }) {
                                if userData.landmarks[self.landmarkIndex].isFavorite {
                                    Image("star-filled")
                                        .resizable()
                                        .renderingMode(.template)
                                        .foregroundColor(.yellow)
                                        .accessibility(label: Text("Remove from favorites"))
                                } else {
                                    Image("star-empty")
                                        .resizable()
                                        .renderingMode(.template)
                                        .foregroundColor(.gray)
                                        .accessibility(label: Text("Add to favorites"))
                                }
                            }
                            .frame(width: 20, height: 20)
                            .buttonStyle(PlainButtonStyle())
                        }
                        //

                        Text(landmark.park)
                        Text(landmark.state)
                    }
                    .font(.caption)
                }
            }
            .padding()
            .frame(maxWidth: 700)
        }
    }
}

struct NavigationDetail_Previews: PreviewProvider {
    static var previews: some View {
        NavigationDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}

#center#50%

9.7 在分隔线下方,使用新的描述字段添加有关 landmark 的更多信息。

标题栏移向了 preview 的头部,因为新内容使封闭的垂直 stack 变宽,但最多不超过先前指定的最大 frame 的大小。

NavigationDetail.swift
import SwiftUI

struct NavigationDetail: 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(alignment: .leading, spacing: 12) {
                HStack(alignment: .center, spacing: 24) {
                    CircleImage(image: landmark.image.resizable(), shadowRadius: 4)
                        .frame(width: 160, height: 160)
                    
                    VStack(alignment: .leading) {
                        HStack {
                            Text(landmark.name).font(.title)
                            
                            Button(action: {
                                self.userData.landmarks[self.landmarkIndex]
                                    .isFavorite.toggle()
                            }) {
                                if userData.landmarks[self.landmarkIndex].isFavorite {
                                    Image("star-filled")
                                        .resizable()
                                        .renderingMode(.template)
                                        .foregroundColor(.yellow)
                                        .accessibility(label: Text("Remove from favorites"))
                                } else {
                                    Image("star-empty")
                                        .resizable()
                                        .renderingMode(.template)
                                        .foregroundColor(.gray)
                                        .accessibility(label: Text("Add to favorites"))
                                }
                            }
                            .frame(width: 20, height: 20)
                            .buttonStyle(PlainButtonStyle())
                        }

                        Text(landmark.park)
                        Text(landmark.state)
                    }
                    .font(.caption)
                }
                
                //
                Divider()
                
                Text("About \(landmark.name)")
                    .font(.headline)
                
                Text(landmark.details)
                    .lineLimit(nil)
                //
            }
            .padding()
            .frame(maxWidth: 700)
        }
    }
}

struct NavigationDetail_Previews: PreviewProvider {
    static var previews: some View {
        NavigationDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}

#center#50%

9.8 在详情视图的顶部插入地图,然后将其他内容向上偏移到稍微重叠。

占据视图整个宽度的地图将详情文本推到 preview 底部下方,但仍然存在。

NavigationDetail.swift
import SwiftUI

struct NavigationDetail: View {
    @EnvironmentObject var userData: UserData
    var landmark: Landmark

    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }

    var body: some View {
        ScrollView {
            //
            MapView(coordinate: landmark.locationCoordinate)
                .frame(height: 250)
            //

            VStack(alignment: .leading, spacing: 12) {
                HStack(alignment: .center, spacing: 24) {
                    CircleImage(image: landmark.image.resizable(), shadowRadius: 4)
                        .frame(width: 160, height: 160)
                    
                    VStack(alignment: .leading) {
                        HStack {
                            Text(landmark.name).font(.title)
                            
                            Button(action: {
                                self.userData.landmarks[self.landmarkIndex]
                                    .isFavorite.toggle()
                            }) {
                                if userData.landmarks[self.landmarkIndex].isFavorite {
                                    Image("star-filled")
                                        .resizable()
                                        .renderingMode(.template)
                                        .foregroundColor(.yellow)
                                        .accessibility(label: Text("Remove from favorites"))
                                } else {
                                    Image("star-empty")
                                        .resizable()
                                        .renderingMode(.template)
                                        .foregroundColor(.gray)
                                        .accessibility(label: Text("Add to favorites"))
                                }
                            }
                            .frame(width: 20, height: 20)
                            .buttonStyle(PlainButtonStyle())
                        }

                        Text(landmark.park)
                        Text(landmark.state)
                    }
                    .font(.caption)
                }
                
                Divider()
                
                Text("About \(landmark.name)")
                    .font(.headline)
                
                Text(landmark.details)
                    .lineLimit(nil)
            }
            .padding()
            .frame(maxWidth: 700)
            .offset(x: 0, y: -50) //
    }
}

struct NavigationDetail_Previews: PreviewProvider {
    static var previews: some View {
        NavigationDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}

#center#50%

9.9 添加一个“Open in Maps”按钮,单击会将 Maps yinBiao 打开到该位置。

import SwiftUI

struct NavigationDetail: View {
    @EnvironmentObject var userData: UserData
    var landmark: Landmark

    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }

    var body: some View {
        ScrollView {
            MapView(coordinate: landmark.locationCoordinate)
                .frame(height: 250)

            //
         Button("Open in Maps") {
            let destination = MKMapItem(placemark: MKPlacemark(coordinate: self.landmark.locationCoordinate))
            destination.name = self.landmark.name
            destination.openInMaps()
         }
            //

            VStack(alignment: .leading, spacing: 12) {
                HStack(alignment: .center, spacing: 24) {
                    CircleImage(image: landmark.image.resizable(), shadowRadius: 4)
                        .frame(width: 160, height: 160)
                    
                    VStack(alignment: .leading) {
                        HStack {
                            Text(landmark.name).font(.title)
                            
                            Button(action: {
                                self.userData.landmarks[self.landmarkIndex]
                                    .isFavorite.toggle()
                            }) {
                                if userData.landmarks[self.landmarkIndex].isFavorite {
                                    Image("star-filled")
                                        .resizable()
                                        .renderingMode(.template)
                                        .foregroundColor(.yellow)
                                        .accessibility(label: Text("Remove from favorites"))
                                } else {
                                    Image("star-empty")
                                        .resizable()
                                        .renderingMode(.template)
                                        .foregroundColor(.gray)
                                        .accessibility(label: Text("Add to favorites"))
                                }
                            }
                            .frame(width: 20, height: 20)
                            .buttonStyle(PlainButtonStyle())
                        }

                        Text(landmark.park)
                        Text(landmark.state)
                    }
                    .font(.caption)
                }
                
                Divider()
                
                Text("About \(landmark.name)")
                    .font(.headline)
                
                Text(landmark.details)
                    .lineLimit(nil)
            }
            .padding()
            .frame(maxWidth: 700)
            .offset(x: 0, y: -50)
    }
}

struct NavigationDetail_Previews: PreviewProvider {
    static var previews: some View {
        NavigationDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}

#center#50%

9.10 将“Open in Maps”按钮放置在 overlay 中,使其显示在地图的右下角。

NavigationDetail.swift
import SwiftUI
import MapKit

struct NavigationDetail: View {
    @EnvironmentObject var userData: UserData
    var landmark: Landmark

    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }

    var body: some View {
        ScrollView {
            MapView(coordinate: landmark.locationCoordinate)
                .frame(height: 250)
                //
                .overlay(
                    GeometryReader { proxy in
                  Button("Open in Maps") {
                     let destination = MKMapItem(placemark: MKPlacemark(coordinate: self.landmark.locationCoordinate))
                     destination.name = self.landmark.name
                     destination.openInMaps()
                  }
                        .frame(width: proxy.size.width, height: proxy.size.height, alignment: .bottomTrailing)
                        .offset(x: -10, y: -10)
                    }
            )
                //

            VStack(alignment: .leading, spacing: 12) {
                HStack(alignment: .center, spacing: 24) {
                    CircleImage(image: landmark.image.resizable(), shadowRadius: 4)
                        .frame(width: 160, height: 160)
                    
                    VStack(alignment: .leading) {
                        HStack {
                            Text(landmark.name).font(.title)
                            
                            Button(action: {
                                self.userData.landmarks[self.landmarkIndex]
                                    .isFavorite.toggle()
                            }) {
                                if userData.landmarks[self.landmarkIndex].isFavorite {
                                    Image("star-filled")
                                        .resizable()
                                        .renderingMode(.template)
                                        .foregroundColor(.yellow)
                                        .accessibility(label: Text("Remove from favorites"))
                                } else {
                                    Image("star-empty")
                                        .resizable()
                                        .renderingMode(.template)
                                        .foregroundColor(.gray)
                                        .accessibility(label: Text("Add to favorites"))
                                }
                            }
                            .frame(width: 20, height: 20)
                            .buttonStyle(PlainButtonStyle())
                        }

                        Text(landmark.park)
                        Text(landmark.state)
                    }
                    .font(.caption)
                }
                
                Divider()
                
                Text("About \(landmark.name)")
                    .font(.headline)
                
                Text(landmark.details)
                    .lineLimit(nil)
            }
            .padding()
            .frame(maxWidth: 700)
            .offset(x: 0, y: -50)
        }
    }
}

struct NavigationDetail_Previews: PreviewProvider {
    static var previews: some View {
        NavigationDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}

#center#50%

10. 组合主视图和详情视图

现在我们已经构建了所有组件视图,接下来通过将主视图和详情视图合并到内容视图中来完善 yinBiao。

#center#80%

10.1 在 MacLandmarks 目录中,选中 ContentView.swift 文件。

ContentView.swift
import SwiftUI

struct ContentView: View {

    var body: some View {
        Text("Hello, World!")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

#center#50%

10.2 给选中的 landmark 添加一个属性并用 @State 修饰。

给选中的 landmark 使用可选值可以避免设置默认值。这意味着 yinBiao 的 preview 和初始状态都将在没有选中 landmark 的情况下呈现。

ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var selectedLandmark: Landmark? //

    var body: some View {
        Text("Hello, World!")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

10.3 添加 UserData 作为环境对象。

内容视图不直接依赖于环境中的 UserData ,但是稍后添加的某些子视图会依赖。为了使 preview 工作和编译成功,内容视图需要获取用户 UserData

ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var selectedLandmark: Landmark?

    var body: some View {
        Text("Hello, World!")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(UserData()) //
    }
}

10.4 在 AppDelegate.swift 文件中,为内容视图提供环境对象,让添加到内容视图的子视图能正确编译。

AppDelegate.swift
func applicationDidFinishLaunching(_ aNotification: Notification) {
    let contentView = ContentView().environmentObject(UserData()) //

    window = NSWindow(
        contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
        styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
        backing: .buffered, defer: false)
    window.center()
    window.setFrameAutosaveName("Main Window")
    window.contentView = NSHostingView(rootView: contentView)
    window.makeKeyAndOrderFront(nil)
}

10.5 将导航视图添加为内容视图中的顶级项目,并限制为最小大小。

ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var selectedLandmark: Landmark?

    var body: some View {
        NavigationView {
            Text("Hello, World!")
        }
        .frame(minWidth: 700, minHeight: 300) //
    }
}

struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(UserData())
    }
}

#center#50%

10.6 添加绑定到所选 landmark 的主视图。

当用户在列表视图中进行选择时,该选择会传递到此视图中的 selectedLandmark 属性中。

ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var selectedLandmark: Landmark?

    var body: some View {
        NavigationView {
            NavigationMaster(selectedLandmark: $selectedLandmark) //
        }
        .frame(minWidth: 700, minHeight: 300)
    }
}

struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(UserData())
    }
}

#center#80%

10.7 添加详情视图。

详情视图带有一个非可选的 landmark,因此在传递给详情视图之前,必须确保该值不为 nil。在用户进行选择之前,不会显示详情视图,这就是为什么此步骤与上一步中对 preview 看起来一样的原因。

ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var selectedLandmark: Landmark?

    var body: some View {
        NavigationView {
            NavigationMaster(selectedLandmark: $selectedLandmark)
          
            //
            if selectedLandmark != nil {
                NavigationDetail(landmark: selectedLandmark!)
            }
            //
        }
        .frame(minWidth: 700, minHeight: 300)
    }
}

struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(UserData())
    }
}

#center#80%

10.8 构建并运行 。

尝试更改过滤器设置,或者在详情视图中单击特定 landmark 的收藏指示符,查看内容是如何响应变化。

#center#80%

Made with in Shangrao,China By 老雷

Copyright © devler.cn 1987 - Present

赣ICP备19009883号-1