1. Mark favorite landmarks
모델 클래스에 isFavorite 프로퍼티를 추가한다.
struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String
    var isFavorite: Bool
    
    private var imageName: String
    var image: Image {
        Image (imageName)
    }
    
    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 에서 isFavorite 값에 따라 별 이미지를 표시
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")
                    .foregroundStyle(.yellow)
            }
        }
    }
}
2. Filter the list view
@State property 를 통해 filter 할지 말지 여부 정보를 담는 변수를 선언하고 이에 대한 상태 관리가 된다.
상태 관리가 된다는 것은 이 값이 바뀔 경우 이 값과 관련된 화면이 새로 렌더링된다는 것.
또한 List 에서 표시할 Landmark 데이터를 필터링하여 담는 computed property를 선언한다. 이 computed property도 showFavoritesOnly property를 참조하고 있기 때문에 showFavoritesOnly 값이 변경되면 fliteredLandmarks 값도 변경된다.
computed property에 대한 것은 문서를 읽어볼 것
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/
Documentation
docs.swift.org
struct LandmarkList: View {
    @State private var showFavoritesOnly = true
    
    var filteredLandmarks: [Landmark] {
        landmarks.filter { landmark in
            (!showFavoritesOnly || landmark.isFavorite)
        }
    }
    
    var body: some View {
        NavigationSplitView {
            List(filteredLandmarks) { landmark in
                NavigationLink {
                    LandmarkDetail(landmark: landmark)
                } label: {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationTitle("Landmarks")
        } detail: {
            Text("Select a Landmark")
        }
    }
}
3. Add a control to toggle the state
리스트 안에서 정적 뷰와 동적인 뷰를 결합할 때는 forEach를 추천한다고 한다.
var body: some View {
        NavigationSplitView {
            List {
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }
                ForEach(filteredLandmarks) { landmark in
                    NavigationLink {
                        LandmarkDetail(landmark: landmark)
                    } label: {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }
            .animation(.default, value: filteredLandmarks)
            .navigationTitle("Landmarks")
        } detail: {
            Text("Select a Landmark")
        }
    }
4. Use observation for storage
@Observable을 사용해서 모델 데이터의 변경사항을 바로 view 에 반영할 수 있게 한다.
@Observable
class ModelData {
    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)")
    }
}
5. Adopt the model object in your views
@Observable로 선언한 ModelData를 @Environment property로 선언한다. @Environment property는 부모에게 environment modifier가 적용되어 있을 경우 값을 가져오게 되어 있다.
struct LandmarkList: View {
    @Environment(ModelData.self) var modelData
    @State private var showFavoritesOnly = false
    var filteredLandmarks: [Landmark] {
        modelData.landmarks.filter { landmark in
            (!showFavoritesOnly || landmark.isFavorite)
        }
    }
    
...
ContentView 에서 environment modifier를 추가한다.
@main
struct LandmarksApp: App {
    @State private var modelData = ModelData()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(modelData)
        }
    }
}preview에서 필요한 경우에도 environment modifier를 추가해준다.
@Environment를 사용하게 되면 상위 뷰에서 데이터를 주입한 다음 필요한 하위 뷰에서 간편하게 상위 뷰에서 주입한 데이터에 접근해 상태관리가 용이함!
6. Create a favorite button for each landmark
FavoritButton 을 구현한다.
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)
                .foregroundStyle(isSet ? .yellow : .gray)
        }
    }
}LandmarkDetail 뷰에서 @Envaironment 로 modelData에 접근해서 Favorite Button 에 전달한다.
여기서 사용되는 @Environment, @Binding, @Bindable 등이 헷갈린다면 아래 링크를 참조
https://forums.developer.apple.com/forums/thread/735416
Why @Bindable and not @Binding in … | Apple Developer Forums
Interesting about the change from the early betas! This is my first testing of it so I don't have any earlier experience. I did a quick test and you're absolutely right with @ObservedObject--if I pass a @StateObject into a subview and I don't prefix the va
forums.developer.apple.com
struct LandmarkDetail: View {
    @Environment(ModelData.self) var modelData
    var landmark: Landmark
    var landmarkIndex: Int {
        modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }
    var body: some View {
        @Bindable var modelData = modelData
        ScrollView {
            MapView(coordinate: landmark.locationCoordinate)
                .frame(height: 300)
            CircleImage(image: landmark.image)
                .offset(y: -130)
                .padding(.bottom, -130)
            VStack(alignment: .leading) {
                HStack {
                    Text(landmark.name)
                        .font(.title)
                    FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
                }
                HStack {
                    Text(landmark.park)
                    Spacer()
                    Text(landmark.state)
                }
                .font(.subheadline)
                .foregroundStyle(.secondary)
                Divider()
                Text("About \(landmark.name)")
                    .font(.title2)
                Text(landmark.description)
            }
            .padding()
        }
        .navigationTitle(landmark.name)
        .navigationBarTitleDisplayMode(.inline)
    }
}
'iOS > SwiftUI Tutorial' 카테고리의 다른 글
| SwiftUI Tutorial 6 - Animating views and transitions (1) | 2024.07.08 | 
|---|---|
| SwiftUI Tutorial 5- Drawing paths and shapes (0) | 2024.07.07 | 
| SwiftUI Tutorial 3 - Building lists and navigation (0) | 2024.07.05 | 
| SwiftUI Tutorial 2 - Creating and combining views 2 (0) | 2024.07.04 | 
| SwiftUI Tutorial 2 - Creating and combining views 1 (0) | 2024.07.03 | 
 
                    
                   
                    
                   
                    
                  