본문 바로가기

IT/Swift

SwiftUI 튜토리얼 3 사용자 입력 다루기

SwiftUI Essentials - Handling User Input

 

 

 

Landmarks 앱에서 사용자는 즐겨 찾는 장소를 즐겨찾기 하고, 즐겨 찾기만 표시하도록 목록을 필터링할 수 있습니다. 이 기능을 만들려면 먼저 스위치를 목록에 추가하여 사용자가 즐겨 찾기 한 항목들만 볼 수 있게 한 다음 사용자가 탭 하여 랜드 마크를 즐겨 찾기로 표시하는 별 모양의 버튼을 추가합니다.
스타터 프로젝트를 다운로드하고 이 튜토리얼을 따라해 보시거나 완료된 프로젝트를 열고 코드를 직접 보셔도 됩니다!

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-shaped button that a user taps to flag a landmark as a favorite.

Download the starter project and follow along with this tutorial, or open the finished project and explore the code on your own.

HandlingUserInput.zip
6.33MB

 

 

 

 

 

 

 

즐겨찾기 표시하기(Mark the User’s Favorite Landmarks)

 

사용자에게 즐겨 찾기를 한눈에 보여 줄 수 있도록 목록을 향상시키는 것부터 시작합니다. 각 LandmarkRow에 즐겨 찾는 랜드 마크를 표시하는 별표를 추가합니다.

Begin by enhancing the list to show users their favorites at a glance. Add a star to each LandmarkRow that shows a favorite landmark.

 

 

 

Step 1

StartingPoint 폴더 안에 있는 엑스코드 프로젝트를 여시고 LandmarkRow.swift 파일을 프로젝트 내비게이터에서 선택해주세요.

Open the starting point Xcode project, and then select LandmarkRow.swift in the Project navigator.

 

 

 

Step 2

현재 랜드마크가 즐겨찾기 항목인지 확인하기 위해 spacer뒤에 if문 안에 별 이미지를 추가합니다.

SwiftUI 블록에서는 if문을 사용하여 조건부로 뷰를 포함시킵니다.

After the spacer, add a star image inside an if statement to test whether the current landmark is a favorite.

In SwiftUI blocks, you use if statements to conditionally include views.

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)
            }
        }
    }
}

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

 

 

 

 

Step 3

시스템 이미지는 백터 기반이기 때문에 foregroundColor(_:) 수정사를 사용하여 색상을 변경할 수 있습니다.

별 표시는 landmark의 isFavorite 프로퍼티가 true일 때 나타납니다. 이내용은 따라오는 튜토리얼 내용에 어떻게 수정하는지 보실 수 있습니다.

Because system images are vector based, you can change their color with the foregroundColor(_:) modifier.

The star is present whenever a landmark’s isFavorite property is true. You’ll see how to modify that property later in this tutorial.

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)
                    .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))
    }
}

 

 

 

 

 

리스트 필터링하기(Filter the List View)

모든 목록을 보이게 하거나 즐겨찾기만 보이도록 목록을 커스텀할 수 있습니다.  이 기능을 넣기 위해서는 LandmarkList 타입에 state를 추가해야 합니다.

state는 시간이 지남에 따라 변경될 수 있고 뷰의 동작, 내용 또는 레이아웃에 영향을 주는 값 또는 값들의 집합입니다. @State 속성과 함께 프로퍼티를 사용하여 뷰에 state를 추가합니다.

You can customize the list view so that it shows all of the landmarks, or just the user’s favorites. To do this, you’ll need to add a bit of state to the LandmarkList type.

State is a value, or a set of values, that can change over time, and that affects a view’s behavior, content, or layout. You use a property with the @State attribute to add state to a view.

 

 

 

 

Step 1

프로젝트 내비게이터에서 LandmarkList.swift를 선택하세요. @State 키워드를 붙인 showFavoritesOnly 프로퍼티를 만들고 초기값으로 false를 할당합니다.

Select LandmarkList.swift in the Project navigator. Add a @State property called showFavoritesOnly to LandmarkList, with its initial value set to false.

import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = false

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

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

 

 

 

 

Step 2

캔버스의 Resume버튼을 사용하여 새로고침을 해보세요.

수정자 프로퍼티가 추가 됐을 때와 같이 뷰 구조가 변경됐을 때는 캔버스를 새로고침 해야 합니다.

Refresh the canvas by clicking the Resume button.

When you make changes to your view’s structure, like adding or modifying a property, you need to manually refresh the canvas.

 

 

 

 

 

Step 3

showFavoritesOnly 프로퍼티와 각 항목의 landmark.isFavorite값을 체크함으로써 랜드마크 목록은 필터링됩니다. 

Filter the list of landmarks by checking the showFavoritesOnly property and each landmark.isFavorite value.

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"))
        }
    }
}

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

 

 

 

 

 

 

토글 메뉴 만들기(Add a Control to Toggle the State)

목록의 필터기능을 사용자에게 제공하기위해 showFavoritesOnly값을 걸러낼수있는 컨트롤을 추가하는것이 필요합니다. 이것은 토글 컨트롤에 바인딩하면 가능합니다.

바인딩은 mutable state에 대한 참조형으로 동작합니다. 사용자가 토글을 키고 끌때 컨트롤은 바인딩을 사용하여 뷰의 state가 업데이트 되도록합니다. 

To give the user control over the list’s filter, you need to add a control that can alter the value of showFavoritesOnly. You do this by passing a binding to a toggle control.

A binding acts as a reference to a mutable state. When a user taps the toggle from off to on, and off again, the control uses the binding to update the view’s state accordingly.

 

 

 

Step 1

중첩된 ForEach 그룹을 생성하여 landmark를 LandmarkRow로 변환합니다.

정적, 동적 뷰를 리스트 안에서 합치거나 많은 다른 동적 뷰 그룹을 결합시키기위해 데이터 리스트 콜렉션을 전달하는 대신에 ForEach를 사용합니다.

Create a nested ForEach group to transform the landmarks into rows.

To combine static and dynamic views in a list, or to combine two or more different groups of dynamic views, use the ForEach type instead of passing your collection of data to List.

import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = true

    var body: some View {
        NavigationView {
            List {
                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()
    }
}

 

 

 

Step 2

토글에 showFavoritesOnly을 바인딩하여 리스트 뷰의 첫번째 자식 뷰로 토글을 추가합니다. 

$접두어를 사용하여 state변수나 프로퍼티에 에 바인딩할수있습니다.

Add a Toggle view as the first child of the List view, passing a binding to showFavoritesOnly.

You use the $ prefix to access a binding to a state variable, or one of its properties.

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 {
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

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

 

 

 

Step 3

미리보기를 사용하여 새로운 토글 기능을 테스트해보세요! 

Use the live preview and try out this new functionality by tapping the toggle.

 

 

 

 

 

 

 

 

 

 

observable 객체 저장하기(Use an Observable Object for Storage)

 

사용자가 즐겨 찾는 특정 랜드 마크를 제어 할 수 있도록 먼저 랜드 마크 데이터를 observable 객체에 저장합니다.
observable 객체는 SwiftUI 환경의 스토리지에서 뷰에 바인딩 될 수있는 데이터의 사용자 정의 객체입니다. SwiftUI는 뷰에 영향을 줄 수있는 observable 객체에 대한 변경 사항을 감시하고 변경 후 뷰의 올바른 버전을 표시합니다.

To prepare for the user to control which particular landmarks are favorites, you’ll first store the landmark data in a observable object.

A observable object is a custom object for your data that can be bound to a view from storage in SwiftUI’s environment. SwiftUI watches for any changes to observable objects that could affect a view, and displays the correct version of the view after a change.

 

 

 

Step 1 
UserData.swift라는 이름으로 새로운 swift파일을 만듭니다.
Create a new Swift file named UserData.swift.

 

 

 

Step 2

Combine 프레임워크를 import하시고 ObservableObject 프로토콜을 따르는 새 모델 유형을 선언하세요.
SwiftUI는 observable 객체를 구독하고 데이터가 변경 될 때 새로 고쳐야하는 모든 뷰를 업데이트합니다.

Declare a new model type that conforms to the ObservableObject protocol from the Combine framework.

SwiftUI subscribes to your observable object, and updates any views that need refreshing when the data changes.

import SwiftUI
import Combine

final class UserData: ObservableObject  {

}

 

 

 

Step 3

showFavoritesOnly 및 landmarks라는 저장 프로퍼티를 초기 값과 함께 추가하세요.

Add stored properties for showFavoritesOnly and landmarks, along with their initial values.

import SwiftUI
import Combine

final class UserData: ObservableObject  {
    var showFavoritesOnly = false
    var landmarks = landmarkData
}

 

 

 

observable 객체는 subscribers가 변경 사항을 알 수 있도록 데이터에 대한 어떠한 변경 사항도 나타내야합니다.

An observable object needs to publish any changes to its data, so that its subscribers can pick up the change.

 

Step 4

@Published 속성을 모델의 각 프로퍼티에 추가하세요.

Add the @Published attribute to each property in the model.

import SwiftUI
import Combine

final class UserData: ObservableObject  {
    @Published var showFavoritesOnly = false
    @Published var landmarks = landmarkData
}

 

 

 

 

 

 

뷰에서 모델 객체 채택하기(Adopt the Model Object in Your Views)

 

UserData 객체를 만들었으므로 뷰를 업데이트하여 앱의 데이터 저장소로 채택해야합니다.

Now that you’ve created the UserData object, you need to update your views to adopt it as the data store for your app.

 

 

 

Step 1

LandmarkList.swift에서 showFavoritesOnly 선언을 @EnvironmentObject 프로퍼티로 바꾸고 environmentObject (_ :) 수정자를 미리보기에 추가하세요.
environmentObject (_ :) 수정자가 부모에 적용되는 동안 userData 프로퍼티는 해당 값을 자동으로 가져옵니다.

In LandmarkList.swift, replace the showFavoritesOnly declaration with an @EnvironmentObject property, and add an environmentObject(_:) modifier to the preview.

This userData property gets its value automatically, as long as the environmentObject(_:) modifier has been applied to a parent.

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 객체의 멤버에 대한 바인딩에 액세스 할 수 있습니다.

Replace the uses of showFavoritesOnly by accessing the same property on userData.

Just like on @State properties, you can access a binding to a member of the userData object by using the $ prefix.

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 {
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

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

 

 

 

 

 

Step 3

ForEach 인스턴스를 작성할 때 userData.landmarks를 데이터로 사용하세요.

Use userData.landmarks as the data when creating the ForEach instance.

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 {
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

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

 

 

 

Step 4

SceneDelegate.swift에서 environmentObject (_ :) 수정자를 LandmarkList에 추가하십시오.
미리보기를 사용하지 않고 시뮬레이터 또는 디바이스에서 랜드 마크를 빌드하고 실행하는 경우 이것은 LandmarkList가 UserData 오브젝트를 갖도록합니다.

In SceneDelegate.swift, add the environmentObject(_:) modifier to the LandmarkList.

If you build and run Landmarks in the simulator or on a device, rather than using the preview, this update ensures that LandmarkList has a UserData object in the environment.

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

UserData 오브젝트와 함께 동작하도록 LandmarkDetail 뷰를 업데이트하세요.
랜드 마크의 즐겨 찾기 상태에 액세스하거나 업데이트 할 때 LandmarkIndex를 사용하여 항상 해당 데이터의 올바른 버전에 액세스 할 수 있습니다.

Update the LandmarkDetail view to work with the UserData object in the environment.

You’ll use landmarkIndex when accessing or updating the landmark’s favorite status, so that you’re always accessing the correct version of that data.

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)
                .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())
    }
}

 

 

 

 

Step 6

LandmarkList.swift로 다시 전환하고 실시간 미리보기를 켜서 모든 것이 제대로 작동하는지 확인해보세요!

Switch back to LandmarkList.swift and turn on the live preview to verify that everything is working as it should be.

 

 

 

 

 

 

랜드마크에 즐겨찾기 버튼 생성하기(Create a Favorite Button for Each Landmark)

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

The Landmarks app can now switch between a filtered and unfiltered view of the landmarks, but the list of favorite landmarks is still hard coded. To allow the user to add and remove favorites, you need to add a favorite button to the landmark detail view.

 

 

 

 

Step 1

LandmarkDetail.swift 파일에서 랜드마크의 name을 HStack안에 배치하세요

In LandmarkDetail.swift, embed the landmark’s name in an 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)
                .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())
    }
}

 

 

 

 

Step 2

랜드 마크 이름 옆에 새 버튼을 만듭니다. if-else 조건문을 사용하여 즐겨찾기 채택 여부를 나타내는 다른 이미지를 제공합니다.
버튼의 액션 클로저에서 코드는 LandmarkIndex를 userData 객체와 함께 사용하여 랜드 마크를 업데이트합니다.

Create a new button next to the landmark’s name. Use an if-else conditional statement to provide different images that indicate whether the landmark is a favorite.

In the button’s action closure, the code uses landmarkIndex with the userData object to update the landmark in place.

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)
                .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())
    }
}

 

 

 

 

Step 3

LandmarkList.swift로 다시 전환하고 실시간 미리보기를 켭니다.
목록에서 세부 사항으로 이동하고 버튼을 누르면 목록으로 돌아갈 때 변경 사항이 유지됩니다. 두 뷰가 환경에서 동일한 모델 객체에 액세스하기 때문에 두 뷰는 일관성을 유지합니다.

Switch back to LandmarkList.swift, and turn on the live preview.

As you navigate from the list to the detail and tap the button, those changes persist when you return to the list. Because both views are accessing the same model object in the environment, the two views maintain consistency.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

#swiftui binding

#swiftui 바인딩

#swiftui란

#swiftui 기초

#swiftui 기본
#swiftui 강좌

#swiftui 강의

#swiftui 교육

#swiftui 시작하기

#swiftui 따라하기

#wwdc2019

#ios programming

#swiftui apple tutorial 번역

#swiftui apple 튜토리얼 번역