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