03 Handing User Input - ly918/SwiftUI-Chinese-Documents GitHub Wiki
处理用户输入
在Landmarks应用程序中,用户可以标记他们最喜欢的位置,并筛选列表来仅仅显示他们最喜欢的位置。要创建此功能,首先要向列表中添加一个开关,以便用户只关注他们的收藏夹,然后添加一个星形按钮,用户点击该按钮可将地标标记到收藏夹。
学习时间:20分钟
第一节 标记用户最喜欢的地标
从优化列表开始,让用户一目了然地看到他们的最爱。为每个显示最喜欢的地标行添加一个星。

步骤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,并打开实时预览。
当您从列表导航到详情信息并点击按钮时,这些更改将在您返回列表时保持不变。因为两个视图都在访问环境中的同一个模型对象,所以两个视图保持一致性。
