处理用户输入
在Landmarks
中,用户可以标记他们喜欢的地点,并在列表中过滤出来。要实现这个功能,我们要先在列表中添加一个开关,这样用户可以只看到他们收藏的内容。另外还会添加一个星形按钮,用户可以点击该按钮来收藏地标。
下载起始项目文件并按照以下步骤操作,也可以打开已完成的项目自行浏览代码。
* 预计完成时间:20 分钟
* 初始项目文件:下载
1. 标记用户收藏的地标
首先,通过优化列表来清晰地给用户显示他们的收藏。给每个被收藏地标的 LandmarkRow
添加一颗星。
1.1 打开起始项目,在项目导航器中选择 LandmarkRow.swift
。
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))
}
}
1.3 由于系统图片是基于矢量的,所以我们可以通过 foregroundColor(_:)
修饰符来修改它们的颜色。
当 landmark
的 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))
}
}
2. 过滤列表视图
我们可以自定义列表视图,让它显示所有的地标或者只显示用户收藏的。为此,我们需要给 LandmarkList
类型添加一些 state
。
state
是一个值或一组值,它可以随时间变化,并且会影响视图的行为、内容或布局。我们用具有 @State
特征的属性将 state
添加到视图中。
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()
}
}
2.2 点击 Resume
按钮来刷新画布。
当我们对视图的结构进行更改,比如添加或修改属性时,需要手动刷新画布。
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()
}
}
3. 添加控件来切换状态
为了让用户控制列表的过滤,我们需要一个可以修改 showFavoritesOnly
值的控件。通过给切换控件传递一个绑定来实现这个需求。
绑定是对可变状态的引用。当用户将状态从关闭切换为打开然后再关闭时,控件使用绑定来更新视图相应的状态。
3.1 创建一个嵌套的 ForEach group
将 landmarks
转换为行视图。
若要在列表中组合静态和动态视图,或者将两个或多个不同的动态视图组合在一起,要使用 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()
}
}
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()
}
}
3.3 使用实时预览并点击切换来尝试这个新功能。
4. 使用 Observable Object 进行存储
为了让用户控制哪些特定地标被收藏,我们先要把地标数据存储在 observable object
中。
observable object
是数据的自定义对象,它可以从 SwiftUI
环境中的存储绑定到视图上。 SwiftUI
监视 observable object
中任何可能影响视图的修改,并在修改后显示正确的视图内容。
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 添加存储属性 showFavoritesOnly
和 landmarks
以及它们的初始值。
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 的数据存储。
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())
}
}
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())
}
}
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
,打开实时预览来验证一切是否正常。
6. 给每个 Landmark 创建收藏按钮
Landmarks
yinBiao 现在可以在已过滤和未过滤的地标视图之间切换,但收藏的地标仍是硬编码的。为了让用户添加和删除收藏,我们需要在地标详情视图中添加收藏夹按钮。
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
中打开预览。
当我们从列表导航到详情并点击按钮时,我们会在返回列表后看到这些更改仍然存在。由于两个视图在环境中访问相同的模型对象,因此这两个视图会保持一致。