第三篇 swiftUI Landmarks 处理用户输入

4,547 阅读7分钟

筛选用户喜欢的地方

在Landmarks中,有一个这样的需求:

  1. 有一个按钮,用户点击可以标记成自己喜欢的地方
  2. 有一个开关,当用户打开开关时,列表显示的就是用户喜欢的所有地方
    现在起始项目文件包含了本次教程需要的初始化项目工程文件

第一节 标记用户喜欢的地标

为了一目了然的显示用户喜欢的地标,您需要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,打开实时预览. 当您从列表导航到详细信息并点击按钮时,当您返回列表时,这些更改会持续存在。由于两个视图在环境中访问相同的模型对象,因此这两个视图保持一致性。