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/
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
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 |