SwiftUI Tutorials
사용자 입력 다루기
Landmark
앱에서 사용자는 본인이 원하는 장소를 선호하는 장소로 표시할 수 있고 해당 장소들로만 리스트를 확인할 수 있습니다. 이러한 기능을 만들려면, 리스트를 전환하는 기능을 추가해야 합니다. 또한, 사용자가 선호하는 장소 랜드마크를 표시할 때 탭할 수 있는 별 모양 버튼을 추가할 것입니다.
Section 1
사용자의 선호 랜드마크 표시하기
사용자의 선호 장소를 한눈에 보여줄 수 있도록 목록을 개선해봅시다. LandmarkRow
뷰에 별을 추가하여 해당 랜드마크가 선호하는 곳임을 보여줄 것입니다.
Step 1
제공한 Xcode 프로젝트 중 starting point 프로젝트를 엽니다. 그리고 LandmarkRow.swift
파일을 프로젝트 네비게이터에서 선택합니다.
Step2
Spacer()
아래에 선호 장소인지 확인하는 if
문을 작성하고 별 이미지를 추가합니다. SwiftUI의 블록에서 if
문은 조건에 따라 뷰를 포함할 것인지를 결정할 수 있습니다.
import SwiftUI
struct LandmarkRow: View {
var landmark: Landmark
var body: some View {
HStack {
landmark.image
.resizable()
.frame(width: 50, height: 50)
Text(landmark.name)
Spacer()
// 선호 장소임을 표시하는 if문추가
if landmark.isFavorite {
Image(systemName: "star.fill")
.imageScale(.medium)
}
//--
}
}
}
...
Step 3
시스템 이미지(system image)는 벡터를 기반으로 하였기 때문에, 여러분은 foregroundColor(_:)
수정자 메서드를 이용하여 벡터 이미지의 컬러를 바꿀 수 있습니다.
별은 랜드마크의 Favorite 속성이 true인지를 표현합니다. 이후의 튜토리얼에서 그 속성을 어떻게 수정하는지를 알아볼 것입니다.
import SwiftUI
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)
// 배경색 변경 수정자 추가
.forgroundColor(.yellow)
//--
}
}
}
}
...
Section 2
리스트 뷰 필터하기
여러분은 리스트 뷰를 커스터마이징하여 모든 랜드마크를 보여주거나 사용자의 선호장소를 표시할 수 있습니다. 이 기능을 구현하려면 LandmarkList
뷰에 @State 어노테이션이 필요합니다.
@State
가 붙은 상태는 값이거나 값의 집합입니다. 이것은 변할 수 있으며 다른 뷰의 동작이나 내용 혹은 레이아웃에 영향을 미칩니다. 여러분은 @State
속성을 이용하여 뷰에 상태를 추가할 수 있습니다.
Step 1
프로젝트 네비게이터에서 LandmarkList.swift
을 선택합니다. @State
속성을 FavoritesOnly
의 이름으로 LandmarkList
뷰에 추가합니다.
import SwiftUI
struct LandmarkList: View {
// @State 속성 추가
@State var showFavortiesOnly = false
// --
var body: some View {
NavigationView {
List(landmarkData) { landmark in
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
Step 2
Resume
버튼을 눌러 캔버스를 새로고침합니다.
속성을 추가하거나 수정하는 등, 뷰의 구조를 변경했다면 캔버스를 직접 새로고침해야할 수도 있습니다.
Step 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 {
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
//--
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
Section 3
상태를 토글하기 위한 컨트롤 추가
사용자에게 리스트를 선호 장소만 필터링할 수 있도록 하려면, showFavorties
의 값을 변경할 수 있는 컨트롤을 추가해야 합니다. 이 값을 변경하는 것은 토글 컨트롤에 _바인딩_을 전달하는 것입니다.
_바인딩(binding)_은 변경될 수 있는 상태(mutable state)에 대한 참조입니다.사용자가 토글을 탭하여 꺼짐에서 켬으로 바거나 끄게될 때, 컨트롤은 바인딩을 이용하여 그 뷰에 대한 상태를 똑같이 수정해줘야 합니다.
Step 1
중첩된 ForEach
그룹을 생성하여 landmarks
를 행으로 변형합니다.
리스트안의 정적 뷰를 동적 뷰와 합치려고 하거나, 동적 뷰의 서로 다른 그룹들을 합치고 싶다면 List
에 컬렉션을 넘기는 대신, ForEach
를 사용하세요.
import SwiftUI
struct LandmarkList: View {
@State var showFavoritesOnly = true
var body: some View {
NavigationView {
// List대신 Foreach가 배열을 순회하도록 함
List {
ForEach(landmarkData) { landmark in
if !self.showFavoritesOnly || landmark.isFavorite {
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
// --
.navigationBarTitle(Text("Landmark"))
}
}
}
Step 2
Toggle
뷰를 List
뷰의 첫 자식 뷰로 추가합니다. 그리고 FavortiesOnly
를 보여주기 위해, 바인딩을 넘겨줍니다.
$
을 상태 변수 맨 앞에 붙여서 바인딩에 접근할 수 있도록 해줍니다.
import SwiftUI
struct LandmarkList: View {
@State var showFavortiesOnly = true
var body: some View {
NavigationView {
List {
// 토글 뷰를 추가
Toggle(isOn: $showFavortiesOnly) {
Text("Favorites Only")
}
// --
ForEach(landmarkData) { landmark in
if !self.showFavortiesOnly || landmark.isFavorite {
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
...
Step 3
라이브 프리뷰를 사용하여 토글을 탭할 때 새로운 기능이 동작하는지 확인하세요.
Section 4
저장을 위한 옵저버블 객체사용하기
사용자가 특정한 랜드마크를 선호지역으로 설정하도록 하기 위해, 여러분은 먼저, _옵저버블 객체(observable object)_에 랜드마크 데이터를 저장해야 합니다.
옵저버블 객체는 커스텀 오브젝트로서, 뷰와 데이터를 연결할 수 있습니다. SwiftUI는 뷰에 영향을 줄 수 있는 옵저버블 객체의 변경을 감지하여 뷰에 정확한 상태를 출력해줍니다.
Step 1
UserData.swift
라는 새로운 스위프트 파일을 생성합니다.
import SwiftUI
Step2
UserData
클래스가 ObservableObject
프로토콜을 따르도록 새로운 모델 타입을 선언합니다. 이 프로토콜은 Combine
프레임워크에 있습니다.
SwiftUI는 여러분의 옵저버블 객체를 감시하고 그 데이터가 변경되면 뷰를 수정합니다.
import SwiftUI
import Combine
final class UserData: ObservableObject {
}
Step 3
landmarks
와 showFavortiesOnly
을 저장 용 속성으로 추가합니다. 또한 초기값을 동시에 부여하도록 합니다.
import SwiftUI
import Combine
final class UserData: ObservableObject {
var showFavortiesOnly = false
var landmarks = landmarkData
}
옵저버블 객체는 그것의 데이터가 변경되면 다시 갱신(publish)되어야 합니다. 따라서 그것을 사용하는 객체가 그 변화를 알아야 합니다.
Step 4
@Published
특성을 각 속성에 추가합니다.
// UserData.swift
import SwiftUI
import Combine
final class UserData: ObservableObject {
@Published var showFavortiesOnly = false
@Published var landmarks = landmarkData
}
Section 5
뷰에 모델 오브젝트 적용하기
이제 UserData
객체를 생성하였으므로 뷰에 이것을 적용하여 앱에 데이터를 저장할 수 있습니다.
Step 1
LandmarkList.swift
에 show FavoritesOnly
로 선언된 부분을 @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 {
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList()
.environmentObject(UserData())
}
}
Step 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(userData.landmark) { landmark in
if !self.userData.showFavortiesOnly || landmark.isFavorites {
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
Step 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")
}
// 옵저버블 객체인 userData로 접근하도록 수정
ForEach(userData.landmarks) { landmark in
if !.self.userData.showFavrotiesOnly || landmark.isFavorites {
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
Step 4
SceneDelegate.swift
에서 environmentObject(_:)
수정자 메서드에 LandmarkList
를 추가합니다.
만약 여러분이 프리뷰를 사용하지 않고, Landmark
앱을 시뮬레이터나 기기에서 실행할 때, 이 코드가 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
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(
rootView: LandmarkList()
.environmentObject(UserData())
)
self.window = window
window.makeKeyAndVisible()
}
}
// ...
}
Step 5
LandmarkDetail
뷰를 수정하여 UserData
객체를 가지고 동작하도록 합니다.
이후, 랜드마크의 선호 상태에 접근하고 그것을 갱신할 때, landmarkIndex
를 사용할 것입니다. 이것을 사용하여 데이터가 정확한 값에 접근할 수 있습니다.
import SwiftUI
struct LandmarkDetail: View {
@EnvironmentObject var userData: UserData // 옵저버블 객체로 변경
var landmark: Landmark
// 랜드마크 favorite 속성에 접근
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
// --
var body: some View {
VStack {
MapView(coordinate: landmark.locationCoordinate)
.edgesIgnoringSafeArea(.top)
.frame(height: 300)
CircleImage(image: landmark.image)
.offset(y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
Text(landmark.name)
.font(.title)
HStack(alignment: .top) {
Text(landmark.park)
.font(.subheadline)
Spacer()
Text(landmark.state)
.font(.subheadline)
}
}
.padding()
Spcaer()
}
.navigationBarTitle(Text(landmark.name), displayMode: .inline)
}
}
struct LandmarkDetail_Preview: PreviewProvider {
static var previews: some View {
LandmarkDetail(landmark: landmarkData[0])
.environmentObject(UserData())
}
}
Step 6
LandmarkList.swift
로 돌아와서 수정한 코드가 정상적으로 동작하는지 확인합니다.
Section 6
랜드마크 선호 버튼 생성하기
랜드마크 앱은 이제 필터링되거나 필터링되지 않은 리스트를 보여줄 수 있습니다. 사용자가 선호지역을 추가하거나 삭제하도록 하려면, 여러분은 랜드마크 상세 뷰에서 선호 버튼을 추가하도록 해야 합니다.
Step 1
LandmarkDetail.swift
을 열고, HStack
안에 랜드마크의 이름을 추가하도록 합니다.
// LandmarkDetail.swift
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(coordinate: landmark.locationCoordinate)
.edgesIgnoringSafeArea(.top)
.frame(height: 300)
CircleImage(image: landmark.iamge)
.offset(y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
// 랜드마크 제목과 별 표시를 담을 HStack 추가
HStack {
Text(landmark.name)
.font(.title)
}
// --
HStack(alignment: .top) {
Text(landmark.park)
.font(.subheadline)
Spacer()
Text(landmark.state)
.font(.subheadline)
}
}
.padding()
Spacer()
}
.navigationBarTitle(Text(landmark.name), displayMode: .inline)
}
}
struct LandmarkDetail_Preivew: PreviewProvider {
static var previews: some View {
LandmarkDetail(landmark: landmarkData[0])
.environmentObject(UserData())
}
}
Step 2
랜드마크의 이름 옆에 새로운 버튼을 생성합니다. if-else
조건 문을 사용하여 랜드마크가 선호 지역인지를 알려주는 이미지를 제공하도록 할 것입니다.
버튼의 action
절의 코드는 landmarkIndex
와 userData
객체를 사용하여 랜드마크를 업데이트합니다.
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(coordinate: landmark.locationCoordinate)
.edgesIgonoringSafeArea(.top)
.frame(height: 300)
CircleImage(image: landmark.image)
.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")
.forgroundColor(Color.yellow)
} else {
Image(systemName: "star")
.foregroundColor(Color.gray)
}
}
// --
}
...
}
}
}
}
Step 3
LandmarkList.swift
로 전환하여 라이브 프리뷰를 실행합니다.
리스트에서 상세 뷰까지를 검사하고 버튼을 탭해보세요. 이러한 변경 사항들이 리스트에 다시 돌아올 때에도 유지됩니다. 왜냐하면 두개의 뷰는 같은 모델 객체에 접근하므로 두 개의 뷰는 일관성을 유지할 수 있는 것 입니다.
Q1) 데이터를 뷰 계층까지 전달하도록 하는 것은?
A1) @EnvironmentObject
(x) environmentObject(_:)
수정자(o)
Q2) 바인딩의 역할은?
A2) 여러 뷰들을 서로 연결하여 그 뷰들이 같은 데이터를 받을 수 있도록 함, (X)
- 값 자체를 의미하고, 값을 변경하는 방법을 의미함
- 바인딩은 값을 저장하는 것을 제어함 따라서 다른 뷰들이 그 값을 읽고 쓰기 위함
원문 : https://developer.apple.com/tutorials/swiftui/handling-user-input