반응형

원문 : SwiftUI Handling User Input

Handling User Input

랜드마크(Landmarks) 앱에서, 사용자가 즐겨찾기 장소에 깃발을 달수 있고, 즐겨찾는 것들만 보여지도록 목록을 필터링 할 수 있습니다. 이 기능을 만들기 위해서, 여러분은 사용자가 자신이 즐겨찾는 것에 집중할 수 있도록 목록을 전환하는 것을 추가하는 것부터 시작하고, 사용자가 랜드마크를 즐겨찾기로 깃발을 지정하는 별표 모양의 버튼을 추가할 것입니다.

시작 프로젝트를 다운로드하고 튜토리얼을 따라하거나, 완성된 프로젝트를 열고 코드를 살펴봅니다.

프로젝트 파일 다운로드

Section 1. 사용자가 즐겨찾는 랜드마크 표시하기(Mark the User’s Favorite Landmarks)

사용자에게 즐겨찾기를 한눈에 보여주기 위해서 목록을 개선하는것부터 시작합니다. 각 LandmarkRow에 즐겨찾는 랜드마크를 보여주는 별을 추가합니다.

1 단계

시작 프로젝트를 Xcode로 열고, 프로젝트 네비게이터에서 LandmarkRow.swift를 선택합니다.

2 단계

공간(spacer) 뒤에, 현재 랜드마크가 즐겨찾기인지 테스트 하기 위해 if문 안쪽에 별 이미지를 추가합니다.

SwiftUI 블록에서, if 문을 사용해서 조건부로 뷰를 포함합니다.

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image(forSize: 50)
            Text(landmark.name)
            Spacer()

            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .imageScale(.medium)
            }
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarkData[0])
            LandmarkRow(landmark: landmarkData[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}

3 단계

시스템 이미지는 벡터 기반이기 때문에, foregroundColor(_:)수정자(modifier)로 색상을 변경 할 수 있습니다.

별은 랜드마크가 isFavorite 프로퍼티가 true인지를 나타냅니다. 이 튜토리얼 뒷부분에서 해당 프로퍼티를 수정하는 방법을 보게 될 것입니다.

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image(forSize: 50)
            Text(landmark.name)
            Spacer()

            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .imageScale(.medium)
                    .foregroundColor(.yellow)
            }
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarkData[0])
            LandmarkRow(landmark: landmarkData[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}

Section 2. 목록 뷰 필터링하기(Filter the List View)

목록 뷰를 모든 랜드마크가 보이게 하거나 사용자가 즐겨찾기 한것만 보이도록 사용자정의 할 수 있습니다. 이를 위해서, LandmarkList 타입에 state을 추가해야 할 것입니다.

State는 하나의 값이거나, 값의 집합이며, 나중에 변경될수 있고, 뷰의 동작, 컨텐츠 레이아웃에 영향을 줍니다. 뷰에 state를 추가하기 위해서 @State 속성 프로퍼티를 사용합니다.

1 단계

프로젝트 네비게이터에서 LandmarkList.swift를 선택합니다. LandmarkList에 showFavoriteOnly라는 @State 프로퍼티를 추가하며, 초기 값은 false입니다.

import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = false

    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}

2 단계

Resume 버튼을 클릭해서 캔버스를 새로고침 합니다.

프로퍼티를 추가하거나 수정하는 것처럼 뷰의 구조를 변경할때, 캔버스를 수동으로 새로고침해야 합니다.

3 단계

showFavoritesOnly 프로퍼티와 각 landmark.isFavorite 값을 확인해서 랜드마크 목록을 필터링합니다.

import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = false

    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                if !self.showFavoritesOnly || landmark.isFavorite {
                    NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}

Section 3. 상태를 토글하기 위한 컨트롤 추가(Add a Control to Toggle the State)

사용자가 목록의 필터 제어할 수 있도록, showFavoritesOnly의 값을 변경할 수 있는 컨트롤을 추가해야 합니다. 토글(toggle) 컨트롤에 바인딩을 전달해서 이를 수행합니다.

바인딩(binding)은 변경가능한 상태에 대한 참조로 동작합니다. 사용자가 토글을 off를 on으로 탭하고, 다시 off로 할때, 그 컨트롤은 바인딩을 사용해서 뷰의 상태를 적절하게 업데이트 합니다.

1 단계

즐겨찾기 행으로 변환하는 중첩된 ForEach 그룹을 만듭니다.

목록에 정적 뷰와 동적 뷰를 결합하거나 두개 이상의 다른 동적 그룹을 결합하려면, List에 데이터의 컬렉션을 전달하는 대신에 ForEach 타입을 사용합니다.

import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = true

    var body: some View {
        NavigationView {
            List {
                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}

2 단계

Toggle 뷰를 List 뷰의 첫번째 자식으로 추가하고, showFavoritesOnly에 바인딩을 전달합니다.

상태 변수나 프로퍼티들중 하나에 대한 바인딩을 위해서 $ 접두사를 사용합니다.

import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = true

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}

3 단계

실시간 미리보기를 사용하고 토글을 탭해서 새로운 기능을 사용해봅니다.

https://docs-assets.developer.apple.com/published/38e272394f/_proj_1_3_live_preview.mp4

Section 4. 저장소에 바인딩가능한 객체 사용하기(Use a Bindable Object for Storage)

사용자가 특정 랜드마크를 즐겨찾기로 제어할 수 있도록 준비하기 위해서, 랜드마크 데이터를 바인딩가능한 객체(bindable object)로 저장할 것입니다.

bindable 객체는 SwiftUI의 환경에서 저장소의 뷰에 바인딩될수 있는 데이터에 대한 사용자정의 객체입니다. SwiftUI는 뷰에 영향을 줄 수 있는 bindable 객체가 변경되는지를 감시하고, 변경된 후에 올바른 버젼의 뷰가 보이도록 합니다.

1 단계

UserData.swift라는 이름으로 새로운 Swift 파일을 생성하고 모델 타입을 선언합니다.

import SwiftUI

final class UserData: BindableObject  {

}

2 단계

필요한 didChange 프로퍼티를 추가하며, PassthroughSubject를 게시자(publisher)로 사용합니다.

PassthroughSubject는 모든 값을 구독자에게 바로 전달하는 Combine 프레임워크의 단순한 게시자입니다. SwiftUI는 이 게시자(publisher)를 통해서 객체를 구독(subscribes)하고, 데이터가 변경될때 새로고침이 필요한 모든 뷰를 업데이트 합니다.

import SwiftUI
import Combine

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()
}

3 단계

showFavoritesOnly와 landmarks에 대한 저장 프로퍼티를 추가하며, 초기값을 줍니다.

import SwiftUI
import Combine

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()

    var showFavoritesOnly = false
    var landmarks = landmarkData
}

클라이언트가 모델의 데이터를 업데이트 할때마다, 하나의 bindable 객체는 구독자에게 알려야 합니다. 두 프로퍼티중 하나가 변경될때, UserData는 didChange 게시자를 통해서 변경사항을 게시해야 합니다.

4 단계

두 프로퍼티에 대한 didChange 게시자를 통해서 보내고 업데이트하는 didSet 처리기를 생성합니다.

import SwiftUI
import Combine

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()

    var showFavoritesOnly = false {
        didSet {
            didChange.send(self)
        }
    }

    var landmarks = landmarkData {
        didSet {
            didChange.send(self)
        }
    }
}

Section 5. 뷰에서 모델 객체 사용하기(Adopt the Model Object in Your Views)

이제 UserData 객체를 만들었으며, 앱에 대한 데이터 저장소처럼 사용하기 위해 뷰를 업데이트해야 합니다.

1 단계

LandmarkList.swift에서, showFavoritesOnly 선언을 @EnvironmentObject프로퍼티로 교체하고, 미리보기에 environmentObject(_:) 수정자를 추가합니다.

userData 프로퍼티는 environmentObject(_:) 수정자가 부모에 적용되어 있는한, 자동으로 값을 가져옵니다.

import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject var userData: UserData

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
            .environmentObject(UserData())
    }
}

2 단계

userData의 동일한 프로퍼티를 사용해서 showFavoritesOnly 용도를 교체합니다.

@State 프로퍼티와 마찬가지로, $ 접두사를 사용해서 userData 객체의 멤버에 바인딩을 사용할 수 있습니다.

import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject var userData: UserData

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $userData.showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(landmarkData) { landmark in
                    if !self.userData.showFavoritesOnly || landmark.isFavorite {
                        NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
            .environmentObject(UserData())
    }
}

3 단계

ForEach 인스턴스를 만들때 userData.landmarks를 데이터처럼 사용합니다.

import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject var userData: UserData

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $userData.showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(userData.landmarks) { landmark in
                    if !self.userData.showFavoritesOnly || landmark.isFavorite {
                        NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
            .environmentObject(UserData())
    }
}

4 단계

SceneDelegate.swift에서, LandmarkList에 environmentObject(_:) 수정자를 추가합니다.

미리보기를 사용하는 대신에, 시뮬레이터나 기기에서 Landmarks를 빌드하고 실행하는 경우에, 그 업데이트는 LandmarkList 환경에서 UserData 객체를 가지고 있는 것을 보장합니다.

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Use a UIHostingController as window root view controller
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UIHostingController(
            rootView: LandmarkList()
                .environmentObject(UserData())
        )
        self.window = window
        window.makeKeyAndVisible()
    }

    // ...
}

5 단계

그 환경에서 UserData 객체로 작업하기 위해서 LandmarkDetail 뷰를 업데이트합니다.

랜드마크의 즐겨찾기 상태를 사용하거나 업데이트할때 landmarkIndex를 사용할 것이며, 항상 올바른 버젼의 데이터를 사용합니다.

import SwiftUI

struct LandmarkDetail: View {
    @EnvironmentObject var userData: UserData
    var landmark: Landmark

    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }

    var body: some View {
        VStack {
            MapView(landmark: landmark)
                .frame(height: 300)

            CircleImage(image: landmark.image(forSize: 250))
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(caption)
                    Spacer()
                    Text(landmark.state)
                        .font(.caption)
                }
            }
            .padding()

            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)
    }
}

struct LandmarkDetail_Preview: PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}

6 단계

LandmarkList.swift로 돌아가고 모든것이 제대로 동작하는지 확인하기 위해서 미리보기를 켭니다.

Section 6. 각 랜드마크에 대한 즐겨찾기 버튼 생성하기(Create a Favorite Button for Each Landmark)

Landmarks 앱은 이제 필터된 랜드마크 뷰와 필터링 되지 않은 랜드마크 뷰간에 전환할 수 있지만, 랜드마크 즐겨찾기 목록은 여전히 하드코딩되어 있습니다. 사용자가 즐겨찾기를 추가하고 제거하도록 하기 위해, 랜드마크 상세 뷰에 즐겨찾기 버튼을 추가해야합니다.

1 단계

LandmarkDetail.swift에서 HStack에 랜드마크의 이름을 포함합니다.

import SwiftUI

struct LandmarkDetail: View {
    @EnvironmentObject var userData: UserData
    var landmark: Landmark

    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }

    var body: some View {
        VStack {
            MapView(landmark: landmark)
                .frame(height: 300)

            CircleImage(image: landmark.image(forSize: 250))
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                HStack {
                    Text(landmark.name)
                        .font(.title)
                }

                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(caption)
                    Spacer()
                    Text(landmark.state)
                        .font(.caption)
                }
            }
            .padding()

            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)
    }
}

struct LandmarkDetail_Preview: PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}

2 단계

랜드마크의 이름 다음에, 새로운 버튼을 생성합니다. 랜드마크가 즐겨찾기되었는지를 나타내는 다른 이미지를 제공하기 위해서 if-else 조건문을 사용합니다.

버튼의 action 클로져에서, userData 객체와 landmarkIndex 코드를 사용해서 랜드마크를 업데이트 합니다.

import SwiftUI

struct LandmarkDetail: View {
    @EnvironmentObject var userData: UserData
    var landmark: Landmark

    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }

    var body: some View {
        VStack {
            MapView(landmark: landmark)
                .frame(height: 300)

            CircleImage(image: landmark.image(forSize: 250))
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                HStack {
                    Text(landmark.name)
                        .font(.title)

                    Button(action: {
                        self.userData.landmarks[self.landmarkIndex].isFavorite.toggle()
                    }) {
                        if self.userData.landmarks[self.landmarkIndex].isFavorite {
                            Image(systemName: "star.fill")
                                .foregroundColor(Color.yellow)
                        } else {
                            Image(systemName: "star")
                                .foregroundColor(Color.gray)
                        }
                    }
                }

                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(caption)
                    Spacer()
                    Text(landmark.state)
                        .font(.caption)
                }
            }
            .padding()

            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)
    }
}

struct LandmarkDetail_Preview: PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}

3 단계

LandmarkList.swift에서 실시간 미리보기를 켭니다.

목록에서 상세정보로 이동하고 버튼을 탭하면, 목록으로 돌아갈때 변경사항이 유지됩니다. 그 환경에서 뷰가 모두 동일한 모델 객체를 사용하기 때문에, 두 뷰는 일관성을 유지합니다.

https://docs-assets.developer.apple.com/published/82df188828/_proj_1_3_complete.mp4

이해하는지 확인하기(Check Your Understanding)

퀴즈 1. 뷰 계층구조에서 데이터를 아래로 전달하는 것은 어떤 것인가?

  1. @EnvionmentObject 속성
  2. environmentObject(_:) 수정자

[정답 : 2]
environmentObject(_:) 수정자를 적용해서 뷰 계층구조에서 아랫쪽에 있는 뷰가 전달된 데이터 객체를 읽을 수 있습니다.

퀴즈 2. 바인딩(binding)의 역할은 무엇인가요?

  1. 값이고 값을 변경하는 방법
  2. 동일한 데이터를 받도록 한쌍의 뷰를 연결하는 방법
  3. 임시로 값을 고정시켜 상태가 전환되는 중에 다른 뷰가 업데이트 하지 못하게 하는 방법

[정답 : 1]
바인딩은 값에 대한 저장소를 제어하므로, 데이터를 읽거나 쓰는 다른 뷰로 데이터를 전달할 수 있습니다.

반응형

'SwiftUI > Tutorials' 카테고리의 다른 글

Interfacing with UIKit  (0) 2019.08.13
Working with UI Controls  (0) 2019.08.13
Composing Complex Interfaces  (0) 2019.08.13
Animating Views and Transitions  (0) 2019.08.13
Drawing Paths and Shapes  (0) 2019.08.13
Building Lists and Navigation  (0) 2019.07.23
Creating and Combining Views  (0) 2019.07.23
Posted by 까칠코더
,