Swift
iOS

SwiftUI 03处理用户输入(Handling User Input)

简介:处理用户输入事件:用户可以标记他们喜欢的地方,并过滤列表以显示他们的最爱

这里记录学习和翻译 Apple SwiftUI 的示例,原文链接

简介

本章是Apple SwiftUI Tutorials的第三个示例,用SwiftUI实现一个叫Landmarks的app。本章要点:用户可以标记他们最喜欢的地方,并过滤列表以仅显示他们最喜欢的地方。要创建此功能,首先要向列表中添加一个开关,以便用户只关注自己的收藏夹,然后添加一个星形按钮,用户点击该按钮可将某个地标标记为收藏夹。

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

首先增强列表,在用户收藏过的行上显示星标,让用户一眼就能看到他们的最爱。

  • 1.列表上每一个item是LandmarkRow类型的视图实现的,所以,打开xcode项目,然后在项目导航器中选择Landmarkrow.swift

1bf9ef39-aec1-449a-bbf6-4a51ad14bbae.png

  • 2.在spacer()方法的下一行添加一个Image视图,用以显示星标。
import SwiftUI

struct LandmarkRow : View {

    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image(forSize: 50)
            Text(landmark.name)

            Spacer()

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

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

在SwiftUI的代码块中,我们可以使用if控制语句包含一些Views。

  • 3.因为系统图像是基于矢量的,所以可以使用foregroundColor()(前景颜色)方法更改它们的颜色。

第2节 筛选列表视图

我们可以自定义List列表视图,以便它显示所有标志,或者只显示用户的收藏夹。要做到这一点,我们需要向LandmarkList类型添加一点状态。

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

600e5b4c-d7c7-44dc-8c0a-642f91b15117.png

  • 1.在项目导航器中选择Landmarklist.swift文件。在LandmarkList类中添加名为showFavoritesOnly@State属性,其初始值设置为false
import SwiftUI

struct LandmarkList : View {

    @State var showFavoritesOnly = false

    var body: some View {
        NavigationView {
            List(landmarkData) { item in
                NavigationButton(destination: LandmarkDetail(landmark: item)) {
                    LandmarkRow(landmark: item)
                }
            }

            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

#if DEBUG
struct LandmarkList_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
#endif
  • 2.点击预览画布窗口的Resume按钮,以预览最新效果

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

  • 3.通过检查ShowFavoritesOnly属性和每个Landmark.IsFavorite值来筛选LandmarksList列表。
import SwiftUI

struct LandmarkList : View {

    @State var showFavoritesOnly = false

    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                if !self.showFavoritesOnly || landmark.isFavorite {
                    NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }

            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

#if DEBUG
struct LandmarkList_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
#endif

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

要让用户控制列表的筛选器,我们需要添加一个可以单独更改showFavoritesOnly值的控件。可以通过将绑定传递给切换控件来实现这一点。
绑定充当对可变状态的引用。当用户点击从关闭切换到打开,然后再次关闭时,控件使用绑定来相应地更新视图的状态。

4dfb8edc-f346-4e2a-b998-677715c0f13f.png

  • 1.创建一个嵌套的ForEach组,将标志转换为行。
import SwiftUI

struct LandmarkList : View {

    @State var showFavoritesOnly = false

    var body: some View {
        NavigationView {
            List {
                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }


            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

#if DEBUG
struct LandmarkList_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
#endif

要在List中组合静态视图和动态视图,或组合两个或多个不同的动态列表或视图时,需要在List中添加ForEach类型,用于显示其中的列表,并将数据集合传递给该ForEach,而不是传递给List

  • 2.添加一个切换视图作为列表视图的第一个子级,将绑定单独传递给showFavoritesOnly
import SwiftUI

struct LandmarkList : View {

    @State var showFavoritesOnly = true

    var body: some View {
        NavigationView {
            List {

                // 视图1 切换按钮
                // 使用$ 前缀 将showFavoritesOnly属性,绑定给Toggle控件
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }


                // 视图2 列表
                /*
                 要在`List`中组合静态视图和动态视图,或组合两个或多个不同的动态列表或视图时,需要在`List`中添加`ForEach`类型,用于显示其中的列表,并将数据集合传递给该`ForEach`,而不是传递给`List`。
                 */
                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }


            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

#if DEBUG
struct LandmarkList_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
#endif

我们可以使用$ 前缀访问绑定到状态变量或其属性之一。

  • 3.使用实时预览并通过点击切换来尝试这个新功能。

屏幕快照2019-06-12下午8.55.38.png

屏幕快照2019-06-12下午8.55.46.png

第4节 使用可绑定对象进行存储

我们需要将用户用户收藏的数据的标志存储到可绑定对象中,以进行持久化存储,方便下次启动app时用户可以查看这些收藏的数据。
可绑定对象是数据的自定义对象,可以从SwiftUI 环境的存储中绑定到视图。SwiftUI 监视对可绑定对象的任何更改,这些更改可能影响视图,并在更改后显示视图的正确版本。

8137b90e-57c5-4cd5-b971-ab0d519ff99b.png

  • 1.创建一个新的名为UserData.swift文件,并创建UserData类,让其遵守BindableObject协议,声明它为模型类型。

BindableObject在SwiftUI 中定义,是一个protocol
用作视图模型的对象,了解更多

import Combine
import SwiftUI


import SwiftUI

final class UserData: BindableObject  {

}
  • 2.在UserData这个Class 中,需要实现BindableObject协议中必需实现的属性didChange,并使用PassthroughSubject类初始化didChange属性。
import Combine
import SwiftUI

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()


}

PassthroughSubjectCombine框架中的一个发布者示例,它立即将任何值传递给其订阅者。 SwiftUI通过此发布者订阅我们的对象,并更新数据更改时需要刷新的所有视图。

  • 3.添加showFavoritesOnly和landmarks的存储属性,并给他们设置初始值。
import SwiftUI
import Combine

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()

    var showFavoritesOnly = false
    var landmarks = landmarkData
}

每当客户端更新模型的数据时,可绑定对象都需要通知其订阅者。 当其中一个属性更改时,UserData应通过其didChange发布者发布更改视图。

  • 4.为通过didChange发布者发送更新的两个属性创建didSet处理程序。
import SwiftUI
import Combine

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()

    var showFavoritesOnly = false {
        didSet {
            didChange.send(self)
        }
    }

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

第5节 在视图中采用模型对象

既然已经创建了UserData对象,那么就需要更新视图,将其作为应用程序的数据存储。

f8ba8b7d-d61b-4292-a771-d4f8ccf602bb.png

  • 1.在LandmarkList.swift中,将showFavoritesOnly的声明由@State替换为@EnvironmentObject,并将environmentObject(_ :)修饰符添加到预览中。

只要将environmentObject(_ :)修饰符应用于父级,此userData属性就会自动获取其值。

Snip20190612_1.png

  • 2.通过访问userData上的相同属性来替换showFavoritesOnly的用法。

就像在@state属性上一样,您可以使用$前缀访问到userdata对象成员的绑定。

  • 3.创建ForEach实例时,使用userData.landmarks作为数据。

  • 4.在SceneDelegate.swift中,将environmentObject(_ :)修饰符添加到LandmarkList

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
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UIHostingController(
            rootView: LandmarkList()
                .environmentObject(UserData())
        )
        self.window = window
        window.makeKeyAndVisible()
    }

    // ...
}

要想看到具有UserData对象的持久化效果时,需要在模拟器或者真机上运行,在预览画布中无法看到这个效果。

  • 5.更新LandmarkDetail视图以使用环境中的UserData对象。

在访问或更新landmark的收藏状态时,我们将使用landmarkIndex,这样您就可以随时访问该数据的存储的正确的持久化数据了。

import SwiftUI

struct LandmarkDetail : View {

    @EnvironmentObject var userData: UserData

    // 从userData 中查找正确的landmark 数据
    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }


    var landmark: Landmark

    var body: some View {

        VStack {

            MapView(coordinate: landmark.locationCoordinate)
                .frame(height: 300)
                .edgesIgnoringSafeArea(.top)
            CircleImage(image: landmark.image(forSize: 250))
                .offset(y: -130)
                .padding(.bottom, -130)
            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                HStack {
                    Text(landmark.park)
                        .font(.subheadline)
                    Spacer()
                    Text(landmark.state)
                        .font(.subheadline)
                }
            }
            .padding()
            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)

    }
}

#if DEBUG
struct LandmarkDetail_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}
#endif
  • 6.切换回LandmarkList.swift并打开实时预览以验证所有内容是否正常工作。

第6节 为每行landmark视图创建收藏按钮

Landmarks应用程序现在可以在已过滤和未过滤的地标视图之间切换,但最喜欢的地标列表仍然是硬编码的。 要允许用户添加和删除收藏夹,我们需要在地标详细信息视图中添加收藏夹按钮。

1146e2d9-6325-4910-92cd-1791bbceda60.png

  • 1.在LandmarkDetail.swift中,将Text(landmark.name)嵌入到HStack中。

  • 2.在landmarkname右侧创建一个新的Button控件。 使用if-else条件语句提供指示地标是否为收藏的不同图像。

import SwiftUI

struct LandmarkDetail : View {

    @EnvironmentObject var userData: UserData

    // 获取当前landmark在userData.landmarks中显示的位置
    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }


    var landmark: Landmark

    var body: some View {

        VStack {

            MapView(coordinate: landmark.locationCoordinate)
                .frame(height: 300)
                .edgesIgnoringSafeArea(.top)
            CircleImage(image: landmark.image(forSize: 250))
                .offset(y: -130)
                .padding(.bottom, -130)
            VStack(alignment: .leading) {
                HStack {
                    Text(landmark.name)
                        .font(.title)
                    Button(action: {
                        // 按钮的事件回调
                        self.userData.landmarks[self.landmarkIndex].isFavorite.toggle()

                    }) {
                        // 这里设置 button 上显示的内容,根据用户是否收藏这个landmark处理显示结果
                        if self.userData.landmarks[landmarkIndex].isFavorite {
                            // 收藏了
                            Image(systemName: "star.fill")
                                .foregroundColor(Color.yellow)
                        }
                        else {
                            // 未收藏
                            Image(systemName: "star")
                                .foregroundColor(Color.gray)
                        }
                    }
                }
                HStack {
                    Text(landmark.park)
                        .font(.subheadline)
                    Spacer()
                    Text(landmark.state)
                        .font(.subheadline)
                }
            }
            .padding()
            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)

    }
}

#if DEBUG
struct LandmarkDetail_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}
#endif

在按钮的动作闭包中,代码使用具有userData对象的landmarkIndex来更新landmark。

  • 3.在LandmarkList.swift中,打开实时预览。

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

测试对以上学习的理解

  • 1.以下哪项在视图层次结构中向下传递数据?

可选项:
A. @EnvironmentObject属性。
B. environmentObject(_ :)修饰符。

答案:B

  • 2.绑定的作用是什么?

可选项:
A. 它是一个值,也是一种改变该值的方法。
B. 这是将一对视图链接在一起以确保它们接收到相同的数据的一种方法。
C. 这是一种临时冻结值的方法,以便在状态转换期间其他视图不会更新。

答案:A

推荐阅读

目录