[SwiftUI] Chapter 1 _3. Handling User Input

Ted·2023년 3월 28일
0

SwiftUI

목록 보기
3/5
post-thumbnail

자자 또 시작해보자.

이번에는 뭐냐 저번에 만든 Landmark앱에서 유저들이 원하는 장소가 있을 것이다. 따라서 유저들이 좋아하는 장소를 필터를 통해서 보여주게 해보는 것이다. 목록에 스위치를 넣고 별모양을 통해서 즐겨찾기를 시도해보자!

Section 1

Mark the User’s Favorite Landmarks

즐겨찾기를 바로 해버리네 오케이 해보자


이렇게 해보는 것이다.

var isFavorite: Bool

landmarkData.json에 나와있는대로 Bool타입으로 선언해준다.
즐겨찾기를 하는 곳은 landmarkRow에 해당하기때문에 landmarkRow.swift로 향한다.

우리는 isFavorite이라는 상태를 정의하였고 이제 적용해야하기에

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

다음과 같은 코드를 넣어준다.
swift에서는 Image의 systemName에 가보면 다양한 icon들을 제공하고 있어서 swift에서 이런 자료를 가져오는 것도 좋은 것같다.

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

만약 ifFavorite가 true이면 별의 상징적인 색인 노랑색으로 안을 채워준다.

근데 여기서 궁금한 점이 생겼다.
foregroundColor는 어떻게 생겼을까? 얘는 또 View의 extension으로 존재한다.

@inlinable public func foregroundColor(_ color: Color?) -> some View

역시 선언없이 바로 값을 넣을 수 있도록 (_:)으로 되어있다. 또한 여기서도 전에 봤던 some이 등장했다. 그리고 nil이 가능한데 사용자 foregroundColor을 제거하고 시스템 또는 컨테이너가 고유 foregroundColor을 제공하도록 하려면 사용하라고 한다.

Section 2

Filter the List View

이번에는 필터를 씌워보는 것이다. 전체 랜드마크가 다 보이게 할 수도 있고 사용자의 즐겨찾기만 보이게 하는 List View를 만들어 볼 것이다.

일단 상태 속성을 저장해놓기 위해서 다음과 같이 state를 정의한다.

@State private var showFavoritesOnly = false

다음은 filter시킨 landmark들만 재배열시켜 정리하기 위해

var filteredLandmarks: [Landmark] {
        landmarks.filter { landmark in
            (!showFavoritesOnly || landmark.isFavorite)
        }
    }

Landmark를 지닌 배열 변수를 만들어준다.

Section 3

Add a Control to Toggle the State

바인딩을 토글을 통해 전달해서 showFavoritesOnly state를 바꾸는 방식으로 작동한다. 바인딩은 mutable 상태에 대한 참조 역할을 한다. 사용자가 해제에서 해제로 전환했다가 다시 해제하면 컨트롤은 바인딩을 사용하여 보기 상태를 적절하게 업데이트해준다.

swift 공식문서 : https://developer.apple.com/documentation/swiftui/binding

State, Binding에 대한 참고자료 :
https://velog.io/@nnnyeong/iOS-SwiftUI-State-Binding

다음은 ForEach를 사용해서 결합하는 것이다.

List {
	ForEach(filteredLandmarks) { landmark in
    	NavigationLink {
        	LandmarkDetail(landmark: landmark)
        } label: {
           	LandmarkRow(landmark: landmark)
        }
    }
}

정적 뷰와 동적 뷰를 목록에 결합하거나 둘 이상의 서로 다른 동적 뷰 그룹을 결합하려면 데이터 컬렉션을 목록에 전달하는 대신 ForEach 유형을 사용한다.

public struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable {

    /// The collection of underlying identified data that SwiftUI uses to create
    /// views dynamically.
    public var data: Data

    /// A function to create content on demand using the underlying data.
    public var content: (Data.Element) -> Content
}

다음 Toggle을 landmarkList.swift 상단에 넣어주고 isOn:에 showFavoritesOnly state를 바인딩 시켜준다.

Toggle(isOn: $showFavoritesOnly) {
	Text("Favorites only")
}

Section 4

Use an Observable Object for Storage

유저가 원하는 특정 랜드마크를 제어하기 위해서 Observable Object를 활용해 저장소에 접근한다.

다음 바로 ModelData.swift에 들어가

import Combine

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

로 Combine 패키지를 불러오고 ModelData 클래스에 ObservableObject를 채택한다.
ObservableObject는 무엇일까?

public typealias ObservableObject = ObservableObject

Section 5

Adopt the Model Object in Your Views

데이터 저장소에 있는 내용을 ModelData object가 계속 보고있다가 변경되면 업데이트를 시킬 수 있도록 만들어보자.

일단 LandmarkList.swift에

@EnvironmentObject var modelData: ModelData

다음 코드를 추가한다.
ModelData는 우리가 직전에 만든 ObservableObject 클래스이며 이를 넣는다. @EnvironmentObject는 또 무엇일까?

@EnvironmentObject는 SwiftUI에서 제공하는 래퍼이다. 일단 뷰간에 데이터를 이동시킬 때 필요한 곳 어디에서나 데이터를 사용하기 용이하게 하기 위해서 사용하게 된다. 만약 데이터가 변경된다면 자동으로 뷰를 업데이트시켜 유지하는 역할도 한다.

@EnvironmentObject 참고자료 : @EnvironmentObject

그리고 landmark의 위치도 변경되었으니 해당 패스에 맞게 변경해준다.

landmarks.filter { landmark in
            (!showFavoritesOnly || landmark.isFavorite)
}
======= 변경전
modelData.landmarks.filter { landmark in
            (!showFavoritesOnly || landmark.isFavorite)
}
=======> 변경후

다음 landmarkDetail, landmarkRow 또한 변경해준다.
어떤 서브뷰에서도 볼 수있게 하기 위해서 ContentView에다가도 environmentObject 속성을 추가해준다.

ContentView()
	.environmentObject(ModelData())

environmentObject(_:) modifier를 LandmarkApp에도 추가해주는데 여기서도 modelData를 추가한다. 다른 점은 @StateObject라는 것을 붙여준다는 것이다.

@StateObject private var modelData = ModelData()

@StateObject의 경우 이 친구에 의해 관찰되고 있는 객체는 그들을 가지고 있는 화면 구조가 재생성되어도 파괴되지않는다고 한다.
따라서 @ObservedObject가 아닌 @StateObject로 만든다.

@StateObject와 @ObservedObject의 차이점을 보면서 공부하면 좋아보인다. 또한 @State와 @Binding의 관계도 같이 보면 좋겠다.

@StateObject 참고자료 : https://pilgwon.github.io/post/state-object-vs-observed-object
@State 참고자료 :
https://velog.io/@nnnyeong/iOS-SwiftUI-State-Binding

Section 6

Create a Favorite Button for Each Landmark

이번에는 즐겨찾기 버튼을 각 label별로 추가하여 isFavorite state를 직접 변경해볼 수 있도록 하는 것이다.

일단 버튼을 생성한다. 아까 toggle을 만들었을때 isFavorite state를 설정했던 것과 같이 @Binding을 통해서 isSet 변수를 만들어준다.

struct FavoriteButton: View {
    @Binding var isSet: Bool

    var body: some View {
        Button {
            isSet.toggle()
        } label: {
            Label("Toggle Favorite", systemImage: isSet ? "star.fill" : "star")
                .labelStyle(.iconOnly)
                .foregroundColor(isSet ? .yellow : .gray)
        }
    }
}

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

isSet에 따른 처리를 위해서 조건부연산자를 통해 간단하게 나타내고 있다. 또한 신기한 녀석을 발견했다. 바로 toggle()이다.

일단 살펴보니 mutating function이라고 한다.

@inlinable public mutating func toggle()

[true, false]로 action을 취하게 해주는 역할인 것같다.

Button의 경우 List와 비슷한 형식을 띄고 있었다.

public struct Button<Label> : View where Label : View {

    /// Creates a button that displays a custom label.
    ///
    /// - Parameters:
    ///   - action: The action to perform when the user triggers the button.
    ///   - label: A view that describes the purpose of the button's `action`.
    public init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)

    /// The content and behavior of the view.
    ///
    /// When you implement a custom view, you must implement a computed
    /// `body` property to provide the content for your view. Return a view
    /// that's composed of built-in views that SwiftUI provides, plus other
    /// composite views that you've already defined:
    ///
    ///     struct MyView: View {
    ///         var body: some View {
    ///             Text("Hello, World!")
    ///         }
    ///     }
    ///
    /// For more information about composing views and a view hierarchy,
    /// see <doc:Declaring-a-Custom-View>.
    @MainActor public var body: some View { get }

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required ``View/body-swift.property`` property.
    public typealias Body = some View
}

Button도 label을 받으며 대신 action이 추가된다. 아마 TextField나 Image View에 action을 추가하면 버튼처럼 만들 수 있던게 이래서 가능하지 않았나 싶다. label: 파트에는 어떠한 View든 가능한가 싶은데 일단 custom view가 가능하다고 되어있다.Button 또한 @MainActor로 메인스레드에서 동작가능하도록 View에서 받는다.

다음은 파일을 다시 보기 쉽게 정리한다.

이젠 LandmarkDetail View에서 아까 만든 FavoriteButton을 넣기 위한 작업을 진행한다.
일단 감시를 위한 데이터를 집어넣고 landmarkIndex를 통해서 관리하도록 생성한다.

@EnvironmentObject var modelData: ModelData

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

다음 HStack을 사용해서 가로로 이름과 버튼을 쌓는다.

HStack {
	Text(landmark.name)
    	.font(.title)
   	FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
}

isSet:을 통해 바인딩된 데이터에 해당하는 별의 모습을 보여준다.
또한 preview에도 modelData를 볼 수 있도록 설정해준다.

struct LandmarkDetail_Previews: PreviewProvider {
    static let modelData = ModelData()

    static var previews: some View {
        LandmarkDetail(landmark: modelData.landmarks[0])
            .environmentObject(modelData)
    }
}

휴 이번에도 힘드러따...

profile
iOS Developer

0개의 댓글