03 Handing User Input - ly918/SwiftUI-Chinese-Documents GitHub Wiki

处理用户输入

Landmarks应用程序中,用户可以标记他们最喜欢的位置,并筛选列表来仅仅显示他们最喜欢的位置。要创建此功能,首先要向列表中添加一个开关,以便用户只关注他们的收藏夹,然后添加一个星形按钮,用户点击该按钮可将地标标记到收藏夹。

学习时间:20分钟

下载示例:HandlingUserInput.zip

第一节 标记用户最喜欢的地标

从优化列表开始,让用户一目了然地看到他们的最爱。为每个显示最喜欢的地标行添加一个星。

步骤1

打开Xcode项目,然后在项目导航器中选择LandmarkRow.swift

步骤2

Spacer()之后,在if语句中添加一个星星Image,用来测试当前地标是否被收藏。

SwiftUI语句块中,使用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))
    }
}

步骤3

由于系统图像是基于矢量的,因此可以使用foregroundColor(_:)修改器更改其颜色。

当地标的isFavorite属性为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))
    }
}

第二节 筛选列表视图

您可以自定义列表视图,使其显示所有地标,或仅显示用户的收藏夹。为此,需要向LandmarkList类型添加@State

@State是一个值或一组值,可以随时间变化,并影响视图的行为、内容或布局。使用带有@State的属性将其添加到视图中。

步骤1

在项目导航器中选择LandmarkList.swift。将名为showFavoritesOnly@State属性添加到LandmarkList,其初始值设置为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()
    }
}

步骤2

单击“Resume”按钮刷新画布。

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

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

第三节 添加控件以切换状态

要让用户控制列表的筛选器,需要添加一个控件,该控件可以单独更改showFavoritesOnly的值。通过绑定toggle控件来完成此操作。

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

步骤1

将行嵌套到ForEach中。

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

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

步骤2

添加一个Toggle视图作为列表视图的第一个子视图,给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()
    }
}

步骤3

使用实时预览并通过点击切换来尝试此新功能。

第四节 使用Observable Object进行存储

为了让用户控制哪些特定的地标是最喜欢的,您首先要将地标数据存储在一个Observable Object中。

Observable Object的自定义对象,可以从SwiftUI环境中的存储绑定到视图。SwiftUI监视可观察对象的任何可能影响视图的更改,并在更改后显示正确的视图。

步骤1

创建一个名为UserData.Swift的新Swift文件。

UserData.swift

import SwiftUI

步骤2

从组合框架中声明遵守ObservableObject协议的新模型类型。

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

UserData.swift

import SwiftUI
import Combine

final class UserData: ObservableObject  {

}

步骤3

添加showFavoritesOnly和地标的存储属性及其初始值。

UserData.swift

import SwiftUI
import Combine

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

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

步骤4

@Published属性添加到模型中的每个属性

UserData.swift

import SwiftUI
import Combine

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

第五节 在视图中采用你的模型对象

现在您已经创建了UserData对象,您需要更新视图以将其作为应用程序的数据存储。

步骤1

LandmarkList.swift中,用@EnvironmentObject属性替换showFavoritesOnly声明,并向预览添加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())
    }
}

步骤2

通过访问userData上的相同属性来替换showFavoritesOnly的使用。

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

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

步骤4

SceneDelegate.swift中,将environmentObject(:)修饰符添加到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

更新LandmarkDetail视图以在环境中使用UserData对象。

在访问或更新地标的收藏状态时,您将使用地标索引,以便始终访问该数据的正确版本。

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

步骤6

切换回LandmarkList.swift并打开实时预览,以验证一切正常工作。

第六节 为每个地标创建收藏夹按钮

地标应用程序现在可以在过滤和未经过滤的地标视图之间切换,但最喜欢的地标列表仍然是硬编码的。要允许用户添加和删除收藏,需要将收藏按钮添加到地标详情视图。

步骤1

LandmarkDetail.swift中,将地标的名称嵌入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())
    }
}

步骤2

在地标名称旁边创建一个新按钮。使用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())
    }
}

步骤3

切换回LandmarkList.swift,并打开实时预览。

当您从列表导航到详情信息并点击按钮时,这些更改将在您返回列表时保持不变。因为两个视图都在访问环境中的同一个模型对象,所以两个视图保持一致性。