Landmarks앱에서 사용자는 즐겨찾는 장소를 즐겨찾기하고, 즐겨찾기만 표시하도록 목록을 필터링할 수 있다. 이 기능을 만들려면 먼저 스위치를 목록에 추가하여 사용자가 즐겨찾기 한 항목들만 볼 수 있게 한 다음 사용자가 탭 하여 랜드마크를 즐겨찾기로 표시하는 별 모양의 버튼을 추가한다.
즐겨찾기 표시하기
사용자에게 즐겨찾기를 한눈에 보여줄 수 있도록 목록을 향상시키는 것 부터 시작한다. 각 LandmarkRow에 즐겨찾는 랜드마크를 표시하는 별표를 추가한다.
LandmarkRow.swift파일을 수정한다. 현재 랜드마크가 즐겨찾기 항목인지 확인하기 위해 spacer뒤 if문 안에 별 이미지를 추가한다.
SwiftUI는 if문을 사용하여 조건부로 뷰를 포함시킨다.
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")
.imageScale(.medium)
}
}
}
}
landmark의 isFavorite에 대한 정보는 JSON파일을 확인하면 알 수 있다.
시스템 이미지가 기본적으로 벡터 이미지이기 때문에 컬러를 foregroundColor()수정자로 변경할 수 있다.
별(이미지)은 랜드마크가 가지고있는 isFavorite프로퍼티가 true일 때에만 나타난다.
2. 리스트 뷰 필터링
리스트 뷰를 커스터마이징 할 수 있다. 모든 랜드마크를 보여주거나 선택한 랜드마크만을 보여줄 수 있다. 이것을 해보기 위해 LandmarkList에 약간의 state를 추가해야 한다.
LandmarkList.swift파일을 선택하여 showFavoriteOnly라 불리는 @State프로퍼티를 LandmarkList에 추가한다. 초기값으로 false를 선언한다.
랜드마크 리스트를 showFavoriteOnly 프로퍼티를 체크하는 것과 landmark.isFavorite값을 체크하여 필터링 한다.
struct LandmarkList: View {
@State var showFavoritesOnly = false
var body: some View {
NavigationView{
List(landmarks){ landmark in
if !self.showFavoritesOnly || landmark.isFavorite{
NavigationLink(destination: LandmarkDetail(landmark: landmark)){
LandmarkRow(landmark: landmark)
}
}
}
.navigationTitle("Landmarks")
}
}
}
State를 토클(on - off)하기 위한 "컨트롤"을 추가한다.
유저에게 리스트의 필터에 대한 권한을 주기 위해 showFavoriteOnly의 값을 바꿀 수 있는 버튼을 추가해야 한다.
바인딩(Binding)을 토글 컨트롤에 전달하여 이 작업을 수행한다.
'Binding'은 변경 가능한 상태에 대한 참조로서의 역할을 한다. 유저가 토글을 해서 on > off 혹은 off > on 할 때 컨트롤은 바인딩을 사용하여 그에 맞춰 뷰의 상태를 업데이트 한다.
중첩된 ForEach그룹을 만들어 랜드마크를 행으로 변형시킨다.
*리스트 내에 정적 뷰와 동적 뷰를 결합하기 위해 혹은 2개 이상의 서로 다른 그룹의 동적 뷰를 결합하기 위해
ForEach타입을 데이터 컬렉션을 List로 전달하는 것 대신 사용한다.
showFavoriteOnly가 true일 때 Landmarks에서 isFavorite이 true인 것만을 필터링 하여 보여줄 리스트도 만들어준다.
import SwiftUI
import MapKit
struct LandmarkList: View {
@State var showFavoritesOnly = false
var filteredLandmarks: [Landmark]{
landmarks.filter{ landmark in
(!showFavoritesOnly || landmark.isFavorite)
}
}
var body: some View {
NavigationView{
List{
Toggle(isOn: $showFavoritesOnly){
Text("Favorites only")
}
ForEach(filteredLandmarks){landmark in
NavigationLink{
LandmarkDetail(landmark: landmark)
}label: {
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList()
}
}
Observable객체 저장하기
사용자가 즐겨찾는 특정 랜드마크를 제어할 수 있도록 랜드 마크 데이터를 observable객체에 저장한다.
observable객체는 SwiftUI환경의 스토리지에서 뷰에 바인딩 될 수 있는 데이터의 사용자 정의 객체이다. SwiftUI는 뷰에 영향을 줄 수 있는 observable객체에 대한 변경 사항을 감시하고 변경 후 뷰의 올바른 버전을 표시한다.
ModelData.swift파일에서 Combine프레임워크를 import하고 ObservableObject 프로토콜을 따르는 새 Model을 선언한다. SwiftUI는 observable객체를 구독하고 데이터가 변경될 때 새로 고쳐야 하는 모든 뷰를 업데이트한다.
landmarks배열을 Model로 이동한다.
Observable객체는 subscribers가 변경 사항을 알 수 있도록 데이터 변경 사항을 게시해야 한다.
landmarks배열에 @Published특성을 추가한다.
@Published는 SwiftUI에서 가장 유용한 속성중 하나이며 변경이 발생할 때 자동으로 알리는 관찰 가능한 observable object를 만들 수 있다. SwiftUI는 이런 변경 사항을 자동으로 모니터링하고 데이터에 의존하는 모든 View의 body속성을 다시 호출한다.
import Foundation
import Combine
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 let error{
fatalError("Couldn't load \(filename) from main bundle: \n\(error)")
}
do{
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch let error{
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
뷰에서 모델 객체 채택하기
개체를 만들었 으므로 View를 업데이트 하여 앱의 데이터 저장소로 채택해야 한다.
LandmarkList.swift에서 @EnvirionmentObject ModelData변수를 추가하고 preview에 수정자를 추가한다.
environmentObject 수정자가 부모에 적용되는 동안 ModelData프로퍼티는 해당 값을 자동으로 가져온다.
랜드마크 필터링 시 데이터로 사용하도록 수정하고 LandmarkDetail에서 Envirionment object에 대해 작업하도록 preview를 수정한다.
import SwiftUI
import MapKit
struct LandmarkList: View {
@Environment var modelData: ModelData
@State var showFavoritesOnly = false
var filteredLandmarks: [Landmark]{
modelData.landmarks.filter{ landmark in
(!showFavoritesOnly || landmark.isFavorite)
}
}
var body: some View {
NavigationView{
List{
Toggle(isOn: $showFavoritesOnly){
Text("Favorites only")
}
ForEach(filteredLandmarks){landmark in
NavigationLink{
LandmarkDetail(landmark: landmark)
}label: {
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList()
.environmentObject(ModelData())
}
}
struct LandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
LandmarkDetail(landmark: ModelData().landmarks[0])
}
}
struct LandmarkRow_Previews: PreviewProvider {
static var landmarks = ModelData().landmarks
static var previews: some View {
Group{
LandmarkRow(landmark: landmarks[0])
LandmarkRow(landmark: landmarks[1])
}
.previewLayout(.fixed(width: 300, height: 70))
}
}
다음으로 시뮬레이터나 기기에서 앱을 실행할 때 환경에 모델개체를 배치하도록 ContentView를 업데이트 한다,
모델 인스턴스를 생성하고 수정자를 사용하여 제공한다.
속성을 사용하여 앱 수명동안 지정된 속성에 대한 모델 개채를 한 번만 초기화할 수 있다.
import SwiftUI
@main
struct LandmarksApp: App {
@StateObject private var modelData = ModelData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelData)
}
}
}
각 랜드마크에 대한 즐겨찾기 버튼 만들기
Landmarks앱은 필터링된 랜드마크 보기와 필터링 되지않은 랜드마크 보기 간에 전환할 수 있지만 즐겨찾는 랜드마크 목록은 여전히 하드코딩 되어있다. 사용자가 즐겨찾기를 추가 및 제거할 수 있도록 하려면 랜드마크 상세보기에 즐겨찾기 버튼을 주가해야 한다.
먼저 FavoriteButton.swift라는 파일을 생성한다. 버튼의 현재 상태를 나타내는 바인딩 변수 isSet을 추가하고 바인딩을 사용하기 때문에 이 뷰 내에서 변경 한 내용은 다시 데이터 소스로 전달된다.
import SwiftUI
struct FavoriteButton: View {
@Binding var isSet: Bool
var body: some View {
//버튼
Button{
//버튼의 action
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))
}
}
Landmark Detail 에서 즐겨찾기 여부를 선택해야하기 때문에 해당 랜드마크의 인덱스를 구한다
랜드마크의 이름과 버튼을 디테일 뷰에 나타내준다
struct LandmarkDetail: View {
@EnvironmentObject var modelData: ModelData
var landmark: Landmark
var landmarkIndex: Int {
modelData.landmarks.firstIndex(where: { $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)
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()
}
.navigationTitle(landmark.name)
.navigationBarTitleDisplayMode(.inline)
}
}
struct LandmarkDetail_Previews: PreviewProvider {
static let modelData = ModelData()
static var previews: some View {
LandmarkDetail(landmark: modelData.landmarks[0])
.environmentObject(modelData)
}
}