티스토리 뷰
오늘은 드디어 3번째 사용자 입력을 다루는 튜토리얼을 해볼 예정입니다.
랜드마크 앱에서 유저가 즐겨찾기 장소를 표시할 수 있고 좋아하는 즐겨찾기만 모아 볼 수 있습니다.
이 기능을 만들기 위해서, 목록에 사용자가 즐겨찾기에만 볼 수 있는 스위치를 추가한 다음 사용자가 랜드마크를 즐겨찾기로 표시하기 위해 탭하는 별 모양의 버튼을 추가합니다.
즉 오늘 해 볼 내용은
1. 즐겨찾기 보기 on/off용 스위치 만들기
2. 각 랜드마크목록에 즐겨찾기를 누를 수 있는 별 모양의 버튼 추가해주기
입니다!!
필요한 리소스는 아래에 들어가셔서 다운 받으시면 됩니다.
https://developer.apple.com/tutorials/swiftui/handling-user-input
Handling User Input | Apple Developer Documentation
In the Landmarks app, a user can flag their favorite places, and filter the list to show just their favorites. To create this feature, you’ll start by adding a switch to the list so users can focus on just their favorites, and then you’ll add a star-sh
developer.apple.com
Section1.
유저의 즐겨찾는 랜드마크 표시하기
사용자에게 즐겨찾기를 한 눈에 보여주기 위해 List를 강화하는 것으로 시작하세요.
Landmark structure에 속성을 추가하여 랜드마크의 즐겨찾기 여부 초기값을 읽은다음,
좋아하는 랜드마크를 보여주는 각 LandmarkRow에 별을 추가하세요.
Step1. Landmark.swift를 를 왼쪽 네비게이터 영역에서 클릭해주세요.
Step2. Landmark구조체에 isFavorite: Bool 프로퍼티를 추가해주세요.
var isFavorite: Bool
Step3. LandmarkROw.swift를 왼쪽 네비게이터 영역에서 클릭해주세요.
Step4. Spacer() 이후에 if문을 통해 isFavorite이 true이면 별모양 이미지를 추가해주세요.
SwiftUI 블록에서는 if 문을 사용하여 조건부로 뷰를 포함합니다.
if landmark.isFavorite {
Image(systemName: "star.fill")
}


이렇게 isFavorite가 true이면 나타나고 false면 없는게 정상입니다!
Step5. 시스템 이미지는 벡터 기반이기 때문에, foregroundColor(_:) modifier로 색상을 변경할 수 있습니다. 노란색으로 바꿔줍시다!
if landmark.isFavorite {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
Section2.
ListView 필터
listView를 모든 랜드마크를 보거나 유저의 즐겨찾는 랜드마크만 볼 수 있도록 커스터마이징 할 수 있습니다.
이렇게 하기 위해서는 LandmarkList 타입에 @State 속성을 추가해야 합니다.
튜토리얼 1에서 공부했다시피
@State 는 시간이 지남에 따라 바뀔 수 있는 값 또는 값 집합이며, 뷰의 행동, 콘텐츠 또는 레이아웃에 영향을 미칩니다.
Step1. 왼쪽 네비게이터 영역에서 LandmarkList.swift 파일을 선택하고 이전 튜토리얼에서 preview를 여러 모바일 기종에서 멀티 미리보기를 지원했었죠!? 그걸 하나의 기종에서 볼 수 있게 지워줍시다!
Step2. false로 초기화된 @State 프로퍼티를 showFavoritesOnly 이름으로 선언해줍시다! body 밖에 선언해주면 됩니다.
@State private var showFavoritesOnly = false
Step3. canvas 미리보기를 새로고침 해주세요!
- 속성을 추가하거나 수정하는 것과 같이 뷰의 구조를 변경할 때마다 캔버스를 수동으로 새로 고쳐야 한다고 합니다.
Step4. showFavoritesOnly 속성과 각 landmark.isFavorite 값을 확인하여 landmarks List의 필터링된 버전을 계산하십시오.
var filterdLandmarks: [Landmark] {
landmarks.filter { Landmark in
(!showFavoritesOnly || Landmark.isFavorite)
}
}
위 식이 뭐냐면! OR연산자 이므로 양쪽 두 값 모두 false일 때만 필터링 되게 됩니다.
showFavoritesOnly의 값이 false이면 ! 연산자가 있으므로 항상 true가 되기 때문에 Landmark의 모든 요소가 필터링 되지 않고 전체 요소가 반환됩니다.
그러나 showFavoritesOnly값이 true이면 항상 false이므로 Landmark.isFavorite가 false인 요소들은 필터링되겠죠!?
그렇게 걸러진 즐겨찾기된 리스트만 받아 나열하는 방법입니다!
Step5. Step4에서 landmarks 리스트의 필터링된 버전을 사용해 리스트를 나타나세요.
var body: some View {
NavigationView {
List(filterdLandmarks) { landmark in
NavigationLink {
LandmarkDetail(landmark: landmark)
} label: {
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
Step6. showFavoritesOnly값을 true로 바꿔보면 즐겨찾기 목록만 나타나는 것을 보실 수 있습니다.
@State private var showFavoritesOnly = true

Section3.
@State속성값을 Toggle시키는 컨트롤 추가하기
사용자가 리스트의 필터를 제어할 수 있도록 하려면, showFavoritesOnly의 값을 변경할 수 있는 컨트롤을 추가해야 합니다.
우리는 토글 컨트롤에 바인딩을 통해 값을 전달합니다.
여기서 바인딩이란 변하는 상태에 대한 참조 행동을 하는것을 말합니다.
사용자가 Switch를 탭하여 On/Off 토글할때 컨트롤은 바인딩을 사용해 뷰의 상태를 즉각적으로 업데이트합니다.
Step1. landmarks를 하나의 행으로 변환하기 위해 중첩된 ForEach 그룹을 만드세요. List에 Section을 나누는 작업이라고 생각하면 될 것 같아요!
var body: some View {
NavigationView {
List{
ForEach(filterdLandmarks) { landmark in
NavigationLink {
LandmarkDetail(landmark: landmark)
} label: {
LandmarkRow(landmark: landmark)
}
}
}
.navigationTitle("Landmarks")
}
}
Step2. List의 첫 번째 자식View로 ToggleView를 추가하고, showFavoritesOnly에 바인딩을 전달합니다.
$ 접두사를 사용하여 @State 변수 또는 그 속성 중 하나에 대한 바인딩에 액세스합니다.
List{
Toggle(isOn: $showFavoritesOnly) {
Text("Favorites only")
}
ForEach(filterdLandmarks) { landmark in
NavigationLink {
LandmarkDetail(landmark: landmark)
} label: {
LandmarkRow(landmark: landmark)
}
}
}
Step3. showFavoritesOnly를 false로 다시 변경해주세요.
Step4. 라이브 미리보기를 사용해 토글을 탭하여 이 새로운 기능을 사용해 보세요.
Section4.
저장을 위해 Observable객체를 사용하기
사용자가 어떤 특정 랜드마크가 즐겨찾기인지 제어할 수 있도록 준비하려면, 먼저 landmark 데이터를 observable 객체에 저장해야 합니다.
Oservable 객체는 SwiftUI 환경의 저장소에서 View에 바인딩될 수 있는 데이터에 대한 커스텀 객체입니다.
SwiftUI는 뷰에 영향을 미칠 수 있는 관찰하고 변경 후 올바른 버전의 view를 표시합니다.
Step1. 왼쪽 네비게이션 영역에서 ModelData.swift를 클릭해주세요.
Step2. 애플에서 제공하는 Combine 프레임 워크를 import하고 Combine 프레임워크의 ObservableObject 프로토콜을 준수하는 새로운 ModelData타입을 선언하십시오.
SwiftUI는 Observable객체를 subscribes(구독)하고, 데이터가 변경될 때 새로 고침이 필요한 뷰를 업데이트합니다. (rx랑 똑같네용)
import Combine
final class ModelData: ObservableObject {
}
Step3. landmarks 배열을 새로 만든 ModelDat 안으로 옮겨주세요.
final class ModelData: ObservableObject {
var landmarks: [Landmark] = load("landmarkData.json")
}
Observable 객체는 subscribers가 변경 사항을 선택할 수 있도록 데이터에 대한 변경 사항을 게시해야 합니다.??
요건 무슨 말일까요.. Combine 공부하러 가야겠어요..
Step4. 랜드마크 배열에 @Published 속성을 추가하세요.
@Published var landmarks: [Landmark] = load("landmarkData.json")
왜 추가해야되는지는 안써있네요.. Combine에 관한 글을 따로 올릴게요 지금은 그냥 따라하겠습니다!
Section5.
당신의 View에서 Model객체를 채택하세요.
이제 ModelData 객체를 만들었으므로, 앱의 데이터 저장소로 채택하려면 뷰를 업데이트해야 합니다.
Step1. LandmarkList.swift에서 뷰에 @EnvironmentObject 속성 선언을 추가하고 아래Preview에 environmentObject(_:) 수정자를 추가하십시오.
@EnvironmentObject var modelData: ModelData
Step2. 랜드마크를 필터링할 때 landmarks 대신 modelData.landmarks를 데이터로 사용하세요.
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList()
.environmentObject(ModelData())
}
}
Step1~2 결과
import SwiftUI
struct LandmarkList: View {
@EnvironmentObject var modelData: ModelData
@State private var showFavoritesOnly = false
var filterdLandmarks: [Landmark] {
modelData.landmarks.filter { Landmark in
(!showFavoritesOnly || Landmark.isFavorite)
}
}
var body: some View {
NavigationView {
List{
Toggle(isOn: $showFavoritesOnly) {
Text("Favorites only")
}
ForEach(filterdLandmarks) { landmark in
NavigationLink {
LandmarkDetail(landmark: landmark)
} label: {
LandmarkRow(landmark: landmark)
}
}
}
.navigationTitle("Landmarks")
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList()
.environmentObject(ModelData())
}
}
Step3. ModelData 객체와 함께 작동하도록 LandmarkDetail View를 업데이트하세요.
struct LandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
LandmarkDetail(landmark: ModelData().landmarks[0])
}
}
Step4. ModelData 객체와 함께 작동하도록 LandmarkRow 미리보기를 업데이트하세요.
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))
}
}
Step5. ContentView 미리보기를 업데이트하여 model객체를 환경에 추가하면 모든 하위 뷰에서 model객체를 사용할 수 있습니다.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ModelData())
}
}
다음으로, 시뮬레이터나 장치에서 앱을 실행할 때 모델 객체를 환경에 넣도록 앱 인스턴스를 업데이트할 것입니다.
Step6. LandmarksApp을 업데이트하여 모델 인스턴스를 만들고 environmentObject(_:) 수정자를 사용하여 ContentView에 제공하십시오.
struct SwiftUI_Tutorial1_LandmarksApp: App {
@StateObject private var modelData = ModelData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelData)
// .background(Color.brown)
// TestView()
// .background(Color.yellow)
}
}
}
Step7. LandmarkList.swift로 다시 전환하고 라이브 미리보기를 켜서 모든 것이 제대로 작동하는지 확인하세요.
Section6.
각 랜드마크 즐겨찾기 버튼 생성하기
랜드마크 앱은 이제 랜드마크의 필터링된 뷰와 필터링되지 않은 뷰를 전환할 수 있지만, 좋아하는 랜드마크 리스트는 여전히 하드 코딩되어 있습니다. 사용자가 즐겨찾기를 추가하고 제거할 수 있도록 하려면, 랜드마크 세부 정보 보기에 즐겨찾기 버튼을 추가해야 합니다.
먼저 재사용 가능한 FavoriteButton을 만들 것입니다.
Step1. FavoriteButton.swift를 생성해주세요.
Step2. 버튼의 현재 상태를 나타내는 isSet 바인딩 속성을 추가하고 미리보기에 일정한 값(true)을 제공합니다.
바인딩을 사용하기 때문에, 이 view 내부의 변경 사항은 데이터 소스로 다시 전파됩니다.
import SwiftUI
struct FavoriteButton: View {
@Binding var isSet: Bool
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct FavoriteButton_Previews: PreviewProvider {
static var previews: some View {
FavoriteButton(isSet: .constant(true))
}
}
Step3. isSet 상태를 전환하는 동작을하고 상태에 따라 모양을 변경하는 버튼을 만드세요.
var body: some View {
Button {
isSet.toggle()
} label: {
Label("Toggle Favorite", systemImage: isSet ? "star.fill" : "star")
.labelStyle(.iconOnly)
.foregroundColor(isSet ? .yellow : .gray)
}
}
버튼Label 에 제공하는 제목 문자열은 iconOnly 라벨 스타일을 사용할 때 UI에 나타나지 않지만, VoiceOver는 접근성을 개선하기 위해 사용합니다.
프로젝트가 성장함에 따라, 계층 구조를 추가하는 것이 좋습니다. 계속하기 전에, 몇 개의 그룹을 더 만드세요.
Step4. 범용 CircleImage.swift, MapView.swift 및 FavoriteButton.swift를 Helpers 그룹으로 수집하고 랜드마크 뷰를 Landmarks그룹으로 수집하십시오.

다음으로, FavoriteButton을 DetailView에 추가하여 버튼의 isSet 속성을 주어진 랜드마크의 isFavorite 속성에 바인딩합니다.
Step5. LandmarkDetail.swift로 전환하고, 모델 데이터와 비교하여 입력 랜드마크의 인덱스를 계산하세요.
이를 지원하기 위해, 당신은 또한 환경의 모델 데이터에 접근해야 합니다.
@EnvironmentObject var modelData: ModelData
var landmark: Landmark
var landmarkIndex: Int {
modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
struct LandmarkDetail_Previews: PreviewProvider {
static let modelData = ModelData()
static var previews: some View {
LandmarkDetail(landmark: ModelData().landmarks[0])
.environmentObject(modelData)
}
}
Step5 결과
import SwiftUI
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)
.frame(height: 300)
.ignoresSafeArea(edges: .top)
CircleImage(image: landmark.image)
.offset(y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
Text(landmark.name)
.font(.title)
HStack {
Text(landmark.park)
Spacer()
Text(landmark.state)
}
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
Text("About \(landmark.name)")
.font(.title2)
Text(landmark.description)
}
.padding()
}
.navigationBarTitle(landmark.name)
.navigationBarTitleDisplayMode(.inline)
}
}
struct LandmarkDetail_Previews: PreviewProvider {
static let modelData = ModelData()
static var previews: some View {
LandmarkDetail(landmark: ModelData().landmarks[0])
.environmentObject(modelData)
}
}
Step6. landmark.name을 출력하는 TextView를 HStack에 삽입하세요 그리고 새로운 FavoriteButton을 다음 자식 뷰로 추가하세요. 달러 기호($)로 isFavorite 속성에 바인딩을 제공하세요.
HStack {
Text(landmark.name)
.font(.title)
FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
}
modelData 객체와 함께 landmarkIndex를 사용하여 버튼이 모델 객체에 저장된 랜드마크의 isFavorite 속성을 업데이트하도록 하십시오.
Step7. LandmarkList.swift로 돌아와서 미리보기로 동작을 확인하세요.
오늘 배운건 아래 두개인데 튜토리얼로는 감이 잘 안잡히네요! 따로 저장 환경에 대한 것과 바인딩에 대해 공부해봐야겠습니다..
1. Binding에 접근할때 $접두사 사용한다.
2. Observable객체는 SwiftUI 환경의 저장소에서 View에 바인딩될 수 있는 데이터에 대한 커스텀 객체이다!
'SwiftUI' 카테고리의 다른 글
[SwiftUI] Beyond scrollviews (0) | 2024.04.16 |
---|---|
[Swift UI] SwiftUI 개발 공식문서 튜토리얼 실습[2] (0) | 2023.03.28 |
[Swift UI] SwiftUI 개발 공식문서 튜토리얼 실습[1] (1) | 2023.03.27 |