代码下载
在Landmark应用中,标记喜爱的地方,过滤地标列表,只显示喜欢的地标。要增加这些特性,首先要在列表上添加一个开关,用来过滤用户喜欢的地标。在地标上添加一个星标按钮,用户可以点击它来标记这个地标为自己喜欢的。
在开始之前先新建项目,将之前 Model、View、Resource 目录及其中的文件复制到项目中,并将 SceneDelegate.swift、Assets.xcassets 文件替换为之前的。
标记用户最喜欢的地标
给地标列表的每一行添加一个星标用来表示用户是否标记该地标为自己喜欢的:
1、选择landmarkData.json文件,为json中的每条数据添加布尔类型的 isFavorite 字段:
[{"name": "Turtle Rock","category": "Featured","city": "Twentynine Palms","state": "California","id": 1001,"park": "Joshua Tree National Park","coordinates": {"longitude": -116.166868,"latitude": 34.011286},"imageName": "turtlerock","isFavorite": true}…
2、选择Landmark.swift文件,为Landmark结构体增加 isFavorite 属性:
var isFavorite: Bool
3、选择LandmarkRow.swift文件,在 Spacer()
后面添加一个if表达式,if表达式判断是否当前地标是用户喜欢的,如果用户标记当前地标为喜欢就显示星标。可以在SwitUI的代码块中使用if语句来条件包含视图,由于系统图片是矢量类型的,可以使用foregroundColor(_:)来改变它的颜色。当地标landmark的isFavorite属性为真时,星标显示:
struct LandmarkRow: View {var landmark: Landmarkvar 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).foregroundStyle(.yellow)}}}
}
过滤列表
可以定制地标列表,让它只显示用户喜欢的地标,或者显示所有的地标。要实现这个功能,需要给LandmarkList视图类型添加一些状态变量。状态(State)是一个值或者一个值的集合,会随着时间而改变,同时会影响视图的内容、行为或布局。在属性前面加上@State修饰词就是给视图添加了一个状态值:
- 选择LandmarkList.swift文件,并给LandmarkList添加一个名为showFavoritesOnly的状态,初始值设置为false
- 点击Resume按钮或快捷键Command+Option+P刷新画布。当对视图进行添加或修改属性等结构性改变时,需要手动刷新画布
- 代码中通过检查showFavoritesOnly属性和每一个地标的isFavorite属性值来过滤地标列表所展示的内容
struct LandmarkList: View {@State var showFavoritesOnly = falsevar body: some View {NavigationView {List(Landmark.list) { landmark inif !self.showFavoritesOnly || landmark.isFavorite {NavigationLink(destination: LandmarkDetail(landmark: landmark)) {LandmarkRow(landmark: landmark)}}}.navigationTitle(Text("Landmarks"))}}
}
添加控件来切换状态
为了让用户控制地标列表的过滤器,需要添加一个可以修改showFavoritesOnly值的控件,传递一个绑定关系给toggle控件可以实现一个绑定关系(binding)是对可变状态的引用。当用户点击toggle控件,从开到关或从关到开,toggle控件会通过绑定关系对应的更新视图的状态:
- 创建一个嵌套的ForEach组来把地标数据转换成地标行视图。在一个列表中组合静态和动态视图,或者组合两个甚至多个不同的动态视图组,使用ForEach类型动态生成而不是给列表传入数据集合生成列表视图
- 添加一个Toggle视图作为列表的每一个子视图,传入一个showFavoritesOnly的绑定关系。使用$前缀来获得一个状态变量或属性的绑定关系,实时预览模式下,点击Toggle控件来验证过滤器的功能
struct LandmarkList: View {@State var showFavoritesOnly = truevar body: some View {NavigationView {List {Toggle(isOn: $showFavoritesOnly, label: {Text("Favorites only")})ForEach(Landmark.list) { landmark inif !self.showFavoritesOnly || landmark.isFavorite {NavigationLink(destination: LandmarkDetail(landmark: landmark)) {LandmarkRow(landmark: landmark)}}}}.navigationTitle(Text("Landmarks"))}}
}
使用可观察对象来存储数据
要实现用户标记哪个地标为自己喜爱的地标这个功能,需要使用可观察对象(observalble object)存放地标数据,可观察对象是一种可以绑定到具体SwifUI视图环境中的数据对象。SwiftUI可以察觉它影响视图展示的任何变化,并在这种变化发生后及时更新对应视图的展示内容。
- 创建一个名为UserData.swift的文件,声明一个遵循ObservableObject协议的新数据模型,ObservableObject协议来自响应式框架Combine。SwiftUI可以订阅可观察对象,并在数据发生改变时更新视图的显示内容
- 添加存储属性showFavoritesOnly和landmarks,并赋予初始值。可观察对象需要对外公布内部数据的任何改动,因此订阅此可观察对象的订阅者就可以获得对应的数据改动信息,给新建的数据模型的每一个属性添加@Published属性修饰词
import SwiftUI
import Combinefinal class UserData: ObservableObject {@Published var showFavoritesOnly = false@Published var landmarks = Landmark.list
}
视图中适配数据模型对象
已经创建了UserData可观察对象,现在要改造视图,让它使用这个新的数据模型来存储视图内容数据:
1、在LandmarkList.swift文件中,使用@EnvironmentObject修饰的userData属性来替换原来的showFavoritesOnly状态属性,并对预览视图调用environmentObject(:)修改器。只要environmentObject(:)修改器应用在视图的父视图上,userData就能够自动获取它的值
2、替换原来使用showFavoritesOnly状态属性的地方,改为使用userData中的对应属性。与@State修饰的属性一样,也可以使用$前缀访问userData对象的成员绑定引用
3、创建ForEach实例时使用userData.landmarks做为数据源
struct LandmarkList: View {@EnvironmentObject var userdata: UserDatavar body: some View {NavigationView {List {Toggle(isOn: $userdata.showFavoritesOnly, label: {Text("Favorites only")})ForEach(userdata.landmarks) { landmark inif !userdata.showFavoritesOnly || landmark.isFavorite {NavigationLink(destination: LandmarkDetail(landmark: landmark)) {LandmarkRow(landmark: landmark)}}}}.navigationTitle(Text("Landmarks"))}}
}#Preview {ForEach(["iPhone SE 3rd generation", "iPhone 15", "iPhone 15 Plus"], id: \.self) { deviceName inLandmarkList().previewDevice(PreviewDevice(rawValue: deviceName)).environmentObject(UserData())}
}
4、在SceneDelegate.swift中,对LandmarkList视图调用environmentObject修改器,这样可以把UserData的数据对象绑定到LandmarkList视图的环境变量中,子视图可以获得父视图环境中的变量。此时如果在模拟器或者真机上运行应用,也可以正常展示视图内容,切换到LandmarkList.swift文件,并打开实时预览视图去验证所添加的功能是否正常工作:
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 controllerif let windowScene = scene as? UIWindowScene {let window = UIWindow(windowScene: windowScene)window.rootViewController = UIHostingController(rootView: LandmarkList().environmentObject(UserData()))self.window = windowwindow.makeKeyAndVisible()}}
为每一个地标创建一个喜爱按钮
Landmark这个应用可以在喜欢和不喜欢的地标列表间进行切换了,但喜欢的地标列表还是硬编码形成的,为了让用户可以自己标记哪个地标是自己喜欢的,需要在地标详情页添加一个标记喜欢的按钮:
- 更新LandmarkDetail视图,让它从父视图的环境变量中取要展示的数据。之后在更新地标的用户喜爱状态时,会用到landmarkIndex这个变量
- 在地标名称的Text控件旁边添加一个新的按钮控件。使用if-else条件语句设置不同的图片显示状态表示这个地标是否被用户标记为喜欢。在Button的动作闭包中,使用了landmarkIndex去修改userData中对应地标的数据
struct LandmarkDetail: View {@EnvironmentObject var userdata: UserDatavar landmarksIndex: Int {userdata.landmarks.firstIndex(where: { $0.id == landmark.id }) ?? 0}var landmark: Landmarkvar body: some View {VStack {MapView(coordinate: landmark.locationCoordinate).edgesIgnoringSafeArea(.top).frame(height: 300)CircleImage(image: landmark.image).offset(y: -130).padding(.bottom, -130)VStack(alignment: .leading) {HStack {Text(landmark.name).font(.title)Button(action: {userdata.landmarks[landmarksIndex].isFavorite.toggle()}, label: {if userdata.landmarks[landmarksIndex].isFavorite {Image(systemName: "star.fill").foregroundStyle(.yellow)} else {Image(systemName: "star").foregroundStyle(.gray)}})}HStack {Text(landmark.park).font(.subheadline)Spacer()Text(landmark.state)}}.padding()Spacer()}.navigationBarTitle(Text(landmark.name), displayMode: .inline)}
}
切换到landmarkList.swift,并开启实时预览模式。当从列表页导航进入详情页后,点击喜欢按钮,喜欢的状态会在返回列表页后与列表中对应的地标喜欢状态保持一致,因为列表页和详情页的地标数据使用的是同一份,所以可以在不同页面间保持状态同步。