본문 바로가기

개발/iOS

[SwiftUI Essentials] Handling User Input - 2

지난 포스팅에서는 데이터에 저장된 Favorites 속성을 읽어 LandmarkRow에 별 이미지를 추가하였고, @State 상태 변수를 사용하여 원하는 조건에 따라 리스트에서 원소를 보이도록 하는 기능을 구현하였다.

 

이번 포스팅에서는 실제로 사용자가 사용할 수 있도록 Toggle을 사용하여 좋아하는 장소만 확인할 수 있도록 하는 기능을 구현하고 사용자가 직접 좋아하는 랜드마크를 표시할 수 있도록 하는 기능을 튜토리얼을 따라가며 구현해보려고한다.

 

Toggle 기능 구현하기

이전에 구현한 기능은 사용자가 코드에서 @State 변수인 `showFavoritesOnly`를 일일히 바꿔줬어야만 가능했다. 하지만 어플리케이션에서 사용자가 직접 코드를 수정할 수 없기 때문에 사용자가 컨트롤 할 수 있도록 만들어줘야 한다. 이를 가능하게 하기 위해 껐다 켰다 할 수 있는 Toggle과 @State를 서로 바인딩(binding) 해주는 작업이 필요하다.

 

이를 위해서 LandmarkList.swift를 수정할 필요가 있다. 우선, List 내에 Toggle을 추가해야 하는데 이를 위해서는 List에 인자로 넘겨줬던 filteredLandmarks를 ForEach문으로 수정해주어야 한다. 그리고 Toggle을 List 내에 추가해주면 된다. isOn에 `showFavoritesOnly`를 인자로 넘겨줘야 하는데 이 때, `$`를 붙여줘서 바인딩을 해주어야 한다. 바인딩을 통해 토글 버튼이 눌렸을 때에 따라서 알맞은 동작을 하게 된다.

 

// LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @State private 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(destination: LandmarkDetail(landmark: landmark)) {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }
            .navigationTitle("Landmarks")
        }
    }
}

 

토글을 통해 원하는 랜드마크만 볼 수 있다.

 

그리고 showFavoritesOnly 변수를 false로 초기화해준다. true로 설정하는 경우 토글 버튼이 꺼져있을 때, 좋아하는 랜드마크들이 표시되고 켰을 때 모든 랜드마크들이 뜨게 되어 원하는 것과 반대로 동작하게 된다. 이 동작을 모두 마치게 되면 이제 사용자가 토글 버튼을 통해서 좋아하는 랜드마크들을 볼 수 있게 된다.

데이터 저장을 위한 Observable Object 사용

`Observable object`는 SwiftUI에서 저장소로부터 view에 bound되는 데이터를 위한 커스텀 객체로 view에서 데이터의 변화가 일어난 경우 변경된 버전을 view에 반영한다.

Observable object를 사용하기 위해 `ModelData.swift` 파일을 수정한다.

 

// ModelData.swift
import Foundation
import Combine

final class ModelData: ObservableObject {
    @Published var landmarks: [Landmark] = load("landmarkData.json")
}

 

Observable object의 사용을 위해서는 Combine 프레임워크를 추가해주어야 한다. ModelData라는 이름의 Observable object를 선언하고, 그 내부에 변화를 관찰하고자 하는 데이터인 landmarks를 옮겨준다. 이 때, 변화를 확인할 수 있도록 하기 위해 앞에 `@Published` 속성을 추가해주어야 한다. 이렇게 작성하고 나면 이제 작성한 view에 모델 객체를 적용해주어야 한다.

LandmarkList.swift의 프로퍼티로 @EnvironmentObject 속성을 갖도록 객체를 추가해주고, App의 시작이 되는 LandmarksApp.swift에서 @StateObject 변수로 생성한 ModelData를 선언하여 .environmentObject로 넘겨준다.

 

// LandmarksApp.swift
import SwiftUI

@main
struct LandmarksApp: App {
    @StateObject private var modelData = ModelData()
    

// LandmarkList.swift
import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject  var modelData: ModelData
    @State private var showFavoritesOnly = false

 

이제 데이터를 사용자가 변형할 데이터를 view에서 사용할 준비가 끝났다.

각 Landmark에 대해 좋아하는 장소 버튼 만들기

이제는 좋아하는 장소를 표시할 수 있는 버튼을 생성해볼 것이다. 사용자는 각 랜드마크의 세부 정보를 보는 LandmarkDetail view에서 별 모양의 버튼을 통해 좋아하는 장소를 저장 또는 해제할 수 있어야 하고, 앱에서는 변경된 데이터를 사용자에게 제공해야 한다.

 

이 작업에서 재사용성을 위해 FavoriteButton.swift를 새로 생성한다. 튜토리얼에서는 그리고 폴더 구조의 리팩토링을 한 번 더 수행한다.

리팩토링은 마음대로 하면 되지만 튜토리얼에서는 Views 폴더 내부에서 랜드마크와 관련있는 view를 Landmarks로, 랜드마크와 관련있는 view들을 위한 view 즉, MapView, CircleImage, FavoriteButton 등의 view를 Helpers로 저장하였다.

 

폴더 구조 리팩토링

 

버튼에서는 바인딩을 위한 `@Binding var isSet: Bool` 프로퍼티를 선언한다. 그리고 이 isSet에 따라서 동작할 버튼을 작성한다.

 

// FavoriteButton.swift
import SwiftUI

struct FavoriteButton: View {
    @Binding var isSet: Bool

    var body: some View {
        Button(action: {
            isSet.toggle()
        }) {
            Image(systemName: isSet ? "star.fill" : "star")
                .foregroundColor(isSet ? Color.yellow : Color.gray)
        }
    }
}

struct FavoriteButton_Previews: PreviewProvider {
    static var previews: some View {
        FavoriteButton(isSet: .constant(true))
    }
}

 

isSet의 true일 때(위), false일 때(아래)

 

버튼은 isSet에서 toggle 동작을 수행하여 isSet의 값에 따라 채워진 노란 별과 비어 있는 회색 별을 나타내게 된다.

이 버튼은 LandmarkDetail에서 사용되게 된다. 튜토리얼에서 데이터의 isFavorite 값에 따라서 다르게 사용할 예정이므로 LandmarkDetail에서도 앞서 사용한 Observable object인 ModelData를 사용해줘야 한다. 이를 작성하면 아래와 같다.

 

// LandmarkDetail.swift
import SwiftUI
import MapKit

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 {
			// ...
            
            VStack(alignment: .leading) {
                HStack {
                    Text(landmark.name)
                        .font(.title)
                        .foregroundColor(.primary)
                    FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
                }
                
                // ...
             }
             
             //...
             
    }
}

 

LandmarkList에서 사용한 것과 같이 `@EnvironmentObject`를 선언해준다. 그리고 LandmarkList에서 어떤 랜드마크가 눌렸는지 확인하기 위해 인덱스 값을 갖고 있는 landmarkIndex를 프로퍼티로 선언한다. 이 구문에서는 넘겨받은 데이터와 모델의 데이터의 인덱스를 비교하여 넘겨받은 인덱스 값을 갖게 된다.

 

그리고 제목 옆에 버튼을 생성하고 이를 위한 isSet에 넘겨받은 인덱스를 사용하여 isFavorite 값을 넘겨준다. 이 버튼은 앞서 선언한 코드에서 확인할 수 있듯이 누를 경우 toggle 동작을 수행한다. 누를 경우 기존 저장된 값을 뒤집는다. 이렇게 하면 이제 작업이 완성되었다.

 

완성된 결과물

 

이처럼 버튼을 통해 좋아하는 랜드마크를 표시할 수 있게 된다.

완성된 어플리케이션에서 Observable object에 의해서 좋아하는 랜드마크에 대한 정보가 앱에 반영되고 있다. 그렇다고 원본 데이터를 변경하는 동작을 수행하지는 않는다. 다시 실행할 경우, 초기에 저장된 대로 화면이 불려오는 것을 확인할 수 있다.

 


 

이렇게 SwiftUI Essentials에 해당하는 튜토리얼에서 제공하는 내용을 모두 정리하였다. 튜토리얼을 하면 할 수록 스토리보드에 비해서 편리하다는 것이다. 직관적인 스토리보드도 좋지만 SwiftUI를 알게 된 이상 앞으로 iOS는 당분간 SwiftUI를 사용할 것 같다. 이제 배운 내용을 복습해야 할 때다. 공공데이터 API와 배운 내용을 활용하여 작은 프로젝트를 하나 진행해야겠다.

반응형