#center#80%

处理用户输入

Landmarks 中,用户可以标记他们喜欢的地点,并在列表中过滤出来。要实现这个功能,我们要先在列表中添加一个开关,这样用户可以只看到他们收藏的内容。另外还会添加一个星形按钮,用户可以点击该按钮来收藏地标。

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

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

1. 标记用户收藏的地标

首先,通过优化列表来清晰地给用户显示他们的收藏。给每个被收藏地标的 LandmarkRow 添加一颗星。

#center#80%

1.1 打开起始项目,在项目导航器中选择 LandmarkRow.swift

#center#80%

1.2 在 spacer 的下面添加一个 if 语句,在其中添加一个星形图片来测试当前地标是否被收藏。

SwiftUI block 中,我们使用 if 语句来有条件的引入视图。

LandmarkRow.swift
import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)
            //
            Spacer()

            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .imageScale(.medium)
            }
            //
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarkData[0])
            LandmarkRow(landmark: landmarkData[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}

#center#80%

1.3 由于系统图片是基于矢量的,所以我们可以通过 foregroundColor(_:) 修饰符来修改它们的颜色。

landmarkisFavorite 属性为 true 时,星星就会显示。稍后我们会在教程中看到如何修改这个属性。

LandmarkRow.swift
import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)
            Spacer()

            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .imageScale(.medium)
                    //
                    .foregroundColor(.yellow)
                    //
            }
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarkData[0])
            LandmarkRow(landmark: landmarkData[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}

#center#80%

2. 过滤列表视图

我们可以自定义列表视图,让它显示所有的地标或者只显示用户收藏的。为此,我们需要给 LandmarkList 类型添加一些 state

state 是一个值或一组值,它可以随时间变化,并且会影响视图的行为、内容或布局。我们用具有 @State 特征的属性将 state 添加到视图中。

#center#80%

2.1 在项目导航器中选择 LandmarkList.swift ,添加一个名叫 showFavoritesOnly@State 属性,把它的初始值设为 false

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    //
    @State var showFavoritesOnly = false
    //

    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

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

#center#30%

2.2 点击 Resume 按钮来刷新画布。

当我们对视图的结构进行更改,比如添加或修改属性时,需要手动刷新画布。

#center#80%

2.3 通过检查 showFavoritesOnly 属性和每个 landmark.isFavorite 的值来过滤地标列表。

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = false

    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                //
                if !self.showFavoritesOnly || landmark.isFavorite {
                    NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                        LandmarkRow(landmark: landmark)
                    }
                }
                //
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

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

#center#30%

3. 添加控件来切换状态

为了让用户控制列表的过滤,我们需要一个可以修改 showFavoritesOnly 值的控件。通过给切换控件传递一个绑定来实现这个需求。

绑定是对可变状态的引用。当用户将状态从关闭切换为打开然后再关闭时,控件使用绑定来更新视图相应的状态。

#center#80%

3.1 创建一个嵌套的 ForEach grouplandmarks 转换为行视图。

若要在列表中组合静态和动态视图,或者将两个或多个不同的动态视图组合在一起,要使用 ForEach 类型,而不是将数据集合传递给 List

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = true

    var body: some View {
        NavigationView {
            //
            List {
                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            //
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

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

#center#30%

3.2 添加一个开关视图作为列表视图的第一个子项,然后给 showFavoritesOnly 传递一个绑定。

我们使用 $ 前缀来访问一个状态变量或者它的属性的绑定。

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = true

    var body: some View {
        NavigationView {
            List {
                //
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }
                //

                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

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

#center#30%

3.3 使用实时预览并点击切换来尝试这个新功能。

#center#30%

4. 使用 Observable Object 进行存储

为了让用户控制哪些特定地标被收藏,我们先要把地标数据存储在 observable object 中。

observable object 是数据的自定义对象,它可以从 SwiftUI 环境中的存储绑定到视图上。 SwiftUI 监视 observable object 中任何可能影响视图的修改,并在修改后显示正确的视图内容。

#center#50%

4.1 创建一个新 Swift 文件,命名为 UserData.swift

UserData.swift
import SwiftUI

4.2 引入 Combine 库,声明一个遵循 ObservableObject 协议的模型类型。

SwiftUI 会订阅您的 ObservableObject ,并在数据更改时更新需要刷新的所有视图。

UserData.swift
import SwiftUI
//
import Combine

final class UserData: ObservableObject  {

}
//

4.3 添加存储属性 showFavoritesOnlylandmarks 以及它们的初始值。

UserData.swift
import SwiftUI
import Combine

final class UserData: ObservableObject  {
    //
    var showFavoritesOnly = false
    var landmarks = landmarkData
    //
}

ObservableObject 需要发布对其数据的任何更改,以便其订阅者可以获取其更改。

4.4 给通过 didChange 发布者发送更新的两个属性创建 didSet handlers

UserData.swift
import SwiftUI
import Combine

final class UserData: ObservableObject  {
    //
    @Published var showFavoritesOnly = false
    @Published var landmarks = landmarkData
    //
}

5. 在视图中接收模型对象

现在已经创建了 UserData 对象,我们需要更新视图来将 UserData 对象用作 yinBiao 的数据存储。

#center#80%

5.1 在 LandmarkList.swift 中,将 showFavoritesOnly 声明换成一个 @EnvironmentObject 属性,然后给 preview 添加一个 environmentObject(_:) 修饰符。

一旦将 environmentObject(_:) 修饰符应用于父级, userData 属性就会自动获取它的值。

LandmarkList.swift
import SwiftUI

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

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

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

5.2 将 showFavoritesOnly 的调用更改成访问 userData 上的相同属性。

@State 属性一样,我们可以使用 $ 前缀访问 userData 对象成员的绑定。

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject var userData: UserData

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

                ForEach(landmarkData) { landmark in
                    //
                    if !self.userData.showFavoritesOnly || landmark.isFavorite {
                    //
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

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

#center#30%

5.3 创建 ForEach 对象时,使用 userData.landmarks 作为其数据。

LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject var userData: UserData

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

                //
                ForEach(userData.landmarks) { landmark in
                //
                    if !self.userData.showFavoritesOnly || landmark.isFavorite {
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

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

#center#30%

5.4 在 SceneDelegate.swift 中,给 LandmarkList 添加 environmentObject(_:) 修饰符。

如果我们不是使用预览,而是在模拟器或真机上构建或运行 Landmarks ,这个更新可以确保 LandmarkList 在环境中持有 UserData 对象。

SceneDelegate.swift
import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Use a UIHostingController as window root view controller
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            //
            window.rootViewController = UIHostingController(
                rootView: LandmarkList()
                    .environmentObject(UserData())
            )
            //
            self.window = window
            window.makeKeyAndVisible()
        }
    }

    // ...
}

5.5 更新 LandmarkDetail 视图来使用环境中的 UserData 对象。

我们使用 landmarkIndex 访问或更新 landmark 的收藏状态,这样就可以始终得到该数据的正确版本。

LandmarkDetail.swift
import SwiftUI

struct LandmarkDetail: View {
    //
    @EnvironmentObject var userData: UserData
    //
    var landmark: Landmark

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

    var body: some View {
        VStack {
            MapView(landmark: landmark)
                .frame(height: 300)

            CircleImage(image: landmark.image)
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(caption)
                    Spacer()
                    Text(landmark.state)
                        .font(.caption)
                }
            }
            .padding()

            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)
    }
}

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

5.6 切回 LandmarkList.swift ,打开实时预览来验证一切是否正常。

#center#80%

6. 给每个 Landmark 创建收藏按钮

Landmarks yinBiao 现在可以在已过滤和未过滤的地标视图之间切换,但收藏的地标仍是硬编码的。为了让用户添加和删除收藏,我们需要在地标详情视图中添加收藏夹按钮。

#center#50%

6.1 在 LandmarkDetail.swift 中,把 landmark.name 嵌套在一个 HStack 中。

LandmarkDetail.swift
import SwiftUI

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

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

    var body: some View {
        VStack {
            MapView(landmark: landmark)
                .frame(height: 300)

            CircleImage(image: landmark.image)
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                //
                HStack {
                    Text(landmark.name)
                        .font(.title)
                }
                //

                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(caption)
                    Spacer()
                    Text(landmark.state)
                        .font(.caption)
                }
            }
            .padding()

            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)
    }
}

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

6.2 在 landmark.name 下面创建一个新按钮。用 if-else 条件语句给地标传递不同的图片来区分是否被收藏。

在按钮的 action 闭包中,代码使用持有 userData 对象的 landmarkIndex 来更新地标。

LandmarkDetail.swift
import SwiftUI

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

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

    var body: some View {
        VStack {
            MapView(landmark: landmark)
                .frame(height: 300)

            CircleImage(image: landmark.image)
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                HStack {
                    Text(landmark.name)
                        .font(.title)

                    //
                    Button(action: {
                        self.userData.landmarks[self.landmarkIndex].isFavorite.toggle()
                    }) {
                        if self.userData.landmarks[self.landmarkIndex].isFavorite {
                            Image(systemName: "star.fill")
                                .foregroundColor(Color.yellow)
                        } else {
                            Image(systemName: "star")
                                .foregroundColor(Color.gray)
                        }
                    }
                    //
                }

                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(caption)
                    Spacer()
                    Text(landmark.state)
                        .font(.caption)
                }
            }
            .padding()

            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)
    }
}

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

6.3 在 LandmarkList.swift 中打开预览。

当我们从列表导航到详情并点击按钮时,我们会在返回列表后看到这些更改仍然存在。由于两个视图在环境中访问相同的模型对象,因此这两个视图会保持一致。

#center#50%

Made with in Shangrao,China By 老雷

Copyright © devler.cn 1987 - Present

赣ICP备19009883号-1