筛选用户喜欢的地方
在Landmarks中,有一个这样的需求:
- 有一个按钮,用户点击可以标记成自己喜欢的地方
- 有一个开关,当用户打开开关时,列表显示的就是用户喜欢的所有地方
现在起始项目文件包含了本次教程需要的初始化项目工程文件
第一节 标记用户喜欢的地标
为了一目了然的显示用户喜欢的地标,您需要Landmark模型中添加一个标记位,当标签位true时,展示一颗星,表示用户收藏即喜欢该地标。
第一步 Landmark模型添加标记
打开Landmark.swift添加bool类型的isFavorite属性
import Foundation
import SwiftUI
import CoreLocation
struct Landmark: Hashable, Codable, Identifiable{
var id: Int
var name: String
var park: String
var state: String
var description: String
var imageName: String
var image: Image {
Image(imageName)
}
var isFarvorite: Bool
private var coordinates: Coordinates
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: coordinates.latitude, longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
}
第二步 展示🌟的逻辑
在LandmarkRow.swift 中添加if 判断语句,当为true时展示🌟
由于系统图像是基于矢量的,因此您可以使用foregroundColor(_:)修饰符更改它们的颜色。
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.isFarvorite {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
}
}
struct LandmarkRow_Previews: PreviewProvider {
static var previews: some View {
// LandmarkRow(landmark: landmarks[0])
Group {
LandmarkRow(landmark: landmarks[0])
LandmarkRow(landmark: landmarks[1])
}
.previewLayout(.fixed(width: 300, height: 70))
}
}
第二节 过滤地标列表
您可以自定义列表视图,使其显示所有地标,或仅显示用户的收藏夹。为此,您需要在LandmarkList中添加一个状态。
状态是一个值,或一组值,可以随着时间的推移而变化,并影响视图的行为、内容或布局。您使用具有@State属性的属性将状态添加到视图中
第一步 添加状态
在LandmarkList.swift中添加一个showFavoritesOnly的状态,其初始值是false,用@State修饰。
由于您使用状态属性来保存特定于视图及其子视图的信息,因此您始终将状态创建为private。
第二步 刷新画布
单击恢复来刷新画布
当您更改视图的结构(如添加或修改属性)时,您需要手动刷新画布。
第三步 过滤逻辑
通过检查showFavoritesOnly属性和每个 landmark.isFavorite值来计算地标列表的过滤版本。
第四步 展示过滤后的地标列表
使用filteredLandmarks地标列表显示
showFavoritesOnly的初始值更改为true,以查看列表的反应。
import SwiftUI
struct LandmarkList: View {
@State private var showFavoritesOnly = false
var filterLandmarks: [Landmark] {
landmarks.filter { landmark in
(!showFavoritesOnly || landmark.isFavorite)
}
}
var body: some View {
NavigationView {
List(filterLandmarks) { landmark in
NavigationLink {
LandmarkDetail(landmark: landmark)
} label: {
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
// LandmarkList()
LandmarkList()
.previewDevice(PreviewDevice(rawValue: "iPhone SE (2nd generation)"))
}
}
第三节 添加一个控件来切换状态
您需要添加一个控件,用来切换showFavorites的值。您需要给开关控件传递一个绑定。
这个绑定引用了可变状态,当用户点击开关从关闭到打开,然后再次关闭时,控件使用绑定相应地更新视图的状态
第一步 创建一个嵌套的For组,将地标转换为行。
要将静态视图和动态视图合并到列表中,或合并两组以上的动态视图,请使用For类型,而不是将数据收集传递给List。
第二步 添加Toggle视图作为List视图的第一个子项,将绑定传递给showFavoritesOnly。
您可以使用$前缀访问状态变量或其属性之一的绑定。
在继续之前,将showFavoritesOnly的默认值返回为false。
mport SwiftUI
struct LandmarkList: View {
@State private var showFavoritesOnly = false
var filterLandmarks: [Landmark] {
landmarks.filter { landmark in
(!showFavoritesOnly || landmark.isFavorite)
}
}
var body: some View {
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly) {
Text("Favorites only")
}
ForEach(filterLandmarks) { landmark in
NavigationLink {
LandmarkDetail(landmark: landmark)
} label: {
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
// LandmarkList()
LandmarkList()
.previewDevice(PreviewDevice(rawValue: "iPhone SE (2nd generation)"))
}
}
第三步 使用实时预览,通过点击开关来尝试此新功能。
第四节 使用可观察对象进行存储
为了让用户收藏地标,首先要将地标数据存储在可观察对象当中。
一个可观察的对象是一个自定义的对象,存储在环境变量中,可以被视图绑定。可观察对象发生变化时更新视图
第一步 修改ModelData.swift
声明ModelData类,遵守ObservableObject
swiftUI会订阅ObservableObject,当数据变化时,更新所有需要刷新的视图。
将landmarks数组放入ModelData类。
第二步
可观察对象需要发布对其数据的任何更改,以便其订阅者可以获取更改。
添加@Published 修饰landmarks数组
import Foundation
final class ModelData: ObservableObject {
@Published var landmarks:[Landmark] = load("landmarkData.json")
}
func load<T: Decodable>(_ fileName: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: fileName, withExtension: nil)
else {
fatalError("Couldn't find \(fileName) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(fileName) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(fileName) as \(T.self):\n\(error)")
}
}
第五节 视图使用模型对象
现在您已经创建了ModelData类,您需要更新视图,以将其用作应用程序的数据存储。
第一步 LandmarkList添加ModelData对象
在LandmarkList.swift中,将@Environment属性声明添加到视图中,并将environmentObject(_:)修饰符添加到预览中。
过滤地标时使用model.landmarks作为数据。
第二步 修改LandmarkDetail
更新LandmarkDetail预览以使用Model对象。
第三步 更新LandmarkRow预览以使用Model对象。
第四步 更新Content预览以将模型对象添加到环境中,使该对象可用于任何子视图。
如果任何子视图在环境中需要模型对象,则预览失败,但您正在预览的视图没有environmentObject(_:)修饰符。
第五步 更新LandmarksApp
更新LandmarksApp以创建模型实例,并使用environmentObject(_:)修饰符将其提供给Content。
在应用程序的生命周期内,使用@StateObject属性仅初始化给定属性的模型对象一次。当您在应用程序实例中使用该属性时(如图所示)以及在视图中使用它时,情况也是如此。
import SwiftUI
@main
struct LandmarksApp: App {
@StateObject private var modelData = ModelData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelData)
}
}
}
第六步 切换回Landmark.swift并打开实时预览,以验证一切是否正常工作。
第六节 为每个地标创建一个收藏按钮
地标应用程序现在可以在地标的过滤视图和未过滤视图之间切换,但最喜欢的地标列表仍然是硬编码的。要允许用户添加和删除收藏夹,您需要在地标详细信息视图中添加收藏夹按钮。
第一步 创建一个名为FavoriteButton.swift的新视图。
1.添加一个isSet绑定,指示按钮的当前状态,并为预览提供一个恒定值。
2.由于您使用绑定,在此视图中所做的更改会传播回数据源。
3.创建一个带有切换isSet状态的操作的Button,并根据状态更改其外观。
4.当您使用iconOnly标签样式时,您为按钮标签提供的标题字符串不会出现在UI中,但旁白(针对盲人)使用它来改善可访问性。
import SwiftUI
struct FavoriteButton: View {
@Binding var isSet: Bool
var body: some View {
Button {
isSet.toggle()
} label: {
Label("Toggle Favorite", systemImage: isSet ? "star.fill" : "star")
.labelStyle(.iconOnly)
.foregroundColor(isSet ? .yellow : .gray)
}
}
}
struct FavoriteButton_Previews: PreviewProvider {
static var previews: some View {
FavoriteButton(isSet: .constant(true))
}
}
第二步 修改LandmarkDetail
切换到LandmarkDetail.swift,通过与模型数据进行比较来计算输入地标的索引。
为了支持这一点,您还需要访问环境的模型数据。
使用新的FavoriteButton将地标的名称嵌入到HStack;使用美元符号($)提供与isFavorite属性的绑定。
import SwiftUI
struct LandmarkDetail: View {
@EnvironmentObject var modelData: ModelData
var landmark: Landmark
var landmarkIndex: Int {
modelData.landmarks.firstIndex { $0.id == landmark.id}!
}
var body: some View {
ScrollView {
MapView(coordinate: landmark.locationCoordinate)
.ignoresSafeArea(edges: .top)
.frame(height: 300)
CircleImage(image: landmark.image)
.offset(y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
.foregroundColor(.black)
FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
}
HStack {
Text(landmark.park)
Spacer()
Text(landmark.state)
}
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
Text("About \(landmark.name)")
.font(.title2)
Text(landmark.description)
}
.padding(.leading, 10)
.padding(.trailing, 10)
Spacer()
}
.navigationTitle(landmark.name)
.navigationBarTitleDisplayMode(.inline)
}
}
struct LandmarkDetail_Previews: PreviewProvider {
static var landmarks = ModelData().landmarks
static var previews: some View {
LandmarkDetail(landmark: landmarks[0])
.environmentObject(ModelData())
}
}
第三步 查看结果
切换回LandmarkList.swift,打开实时预览.
当您从列表导航到详细信息并点击按钮时,当您返回列表时,这些更改会持续存在。由于两个视图在环境中访问相同的模型对象,因此这两个视图保持一致性。