봄날은 갔다. 이제 그 정신으로 공부하자

3-2. SwiftUI Tutorial - ObservableObject, @Published, @ObservedObject, @EnvironmentObject 본문

iOS Tip

3-2. SwiftUI Tutorial - ObservableObject, @Published, @ObservedObject, @EnvironmentObject

길재의 그 정신으로 공부하자 2022. 3. 12. 22:22

이 글은 swiftUI Tutorial 중 3번째 항목인 "Handling User Input" 내용을 기반으로 설명합니다.

전체 소스 및 내용이 궁금하신 분은 아래 링크를 참고해주세요.

https://developer.apple.com/tutorials/swiftui/handling-user-input

 

 

ObservableObject

ObservableObject는 class만 사용 가능한 필수구현을 필요로하지 않는 프로토콜로 Combine 프레임워크의 일부입니다.

ObservableObject를 상속받은 클래스는 @Published를 사용할 수 있는데 @Published를 사용하면 변수의 값이 변경되었을 때 View가 알 수 있으므로 MVVM 아키텍쳐의 ViewModel에 적용하기 좋은 프로토콜 입니다.

사용 방법은 아래와 같습니다.

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

 

@Published

ObservableObject 프로토콜을 준수하면 View를 갱신하기 위해 objectWillChange.send()함수를 호출해야 하는데 값이 변경되어 View 갱신이 필요할 때마다 objectWillChange.send()함수를 호출하는 것은 매우 번거롭고 위험한 일이기 때문에 자동으로 값 변경 시 View를 자동으로 다시 그려주도록 하기 위해 사용하는 어노테이션이 @Published 입니다.

사용 방법은 아래와 같습니다.

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

 

@ObservedObject

@ObservedObject는 ObservableObject 프로토콜을 준수하는 타입에서 사용할 수 있습니다.

즉,MVVM 아키텍쳐에서 ViewModel class는 ObservableObject을 상속받아 설정하고 View에서 @ObservedObject을 사용해 ViewModel값의 변화를 관찰 가능합니다.

사용 방법은 아래와 같습니다.

struct LandmarkList: View {
    @ObservedObject var modelData: ModelData
    @State private var showFavoriteOnly = false
    ...
}

 

@EnvironmentObject

앱의 여러 View와 데이터를 공유해야하는 경우에 SwiftUI에서 사용하는 것이 @EnvironmentObject입니다.

예들 들어 "A View"에서 데이터를 생성한 후 "B View" -> "C View"로 전달하는 경우,

@EnvironmentObject를 사용하면 "A View"값을 생성하면 "B View"와 "C View"에 값을 전달할 필요 없이 공용으로 사용할 수 있습니다.

그리고 @EnvironmentObject사용하면 @ObservedObject를 사용하지 않아도 됩니다.

사용 방법은 아래와 같습니다.

struct LandmarkList: View {
    @EnvironmentObject var modelData: ModelData
    @State private var showFavoriteOnly = false
    ...
}

 

@Environment

@Environment는 View에서 EnvironmentValues의 특정 요소를 읽어와 View 구성에 반영할 때 사용하는 읽기 전용 프로퍼티 wrapper입니다.

 

튜토리얼에서는 ProfileHost View에서 읽기모드인지 아닌지를 판단하기 위해 아래와 같이 사용하고 있습니다.

아래 코드는 EnvironmentValues 하나인  EditMode를 사용해 EditMode가 .active 상태이면 프로필 편집 View(ProfileEditor)를 보여주고 그렇지 않으면 프로필 View(ProfileSummary)를 보여주도록 처리하고 있습니다.

struct ProfileHost: View {
    @Environment(\.editMode) var editMode
    @EnvironmentObject var modelData: ModelData
    @State private var draftProfile = Profile.default

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            HStack {
                if editMode?.wrappedValue == .active {
                    Button("Cancel", role: .cancel) {
                        draftProfile = modelData.profile
                        editMode?.animation().wrappedValue = .inactive
                    }
                }
                Spacer()
                EditButton()
            }

            if editMode?.wrappedValue == .inactive {
                ProfileSummary(profile: modelData.profile)
            } else {
                ProfileEditor(profile: $draftProfile)
                    .onAppear {
                        draftProfile = modelData.profile
                    }
                    .onDisappear {
                        modelData.profile = draftProfile
                    }
            }
        }
        .padding()
    }
}

 

Environment는 시스템에서 제공하는 것외에도 개발자가 직접 필요한 변수를 추가해 사용할 수 있습니다.

 

개발자가 직접 변수를 추가해 사용하기 위해서는 

우선 Environment 프로토콜을 상속받은 타입을 만들고, 

defaultValue타입 프로퍼티를 정의해줘야 합니다. 이 defaultValue는 해당 키에 대한 기본값으로 활용되고 값의 타입을 결정짓습니다.

private struct MyMainThemeColor: EnvironmentKey {
    static let defaultValue = Color(.secondarySystemBackground)
}

EnvironmentValues Type에 사용할 이름의 연산 프로퍼티를 추가한 뒤, getter, setter를 정의해줍니다.

extension EnvironmentValues {
    var myMainThemeColor: Color {
        get { self[MyMainThemeColor.self] }
        set { self[MyMainThemeColor.self] = newValue }
    }
}

 

환경 속성에 대한 View 수정자를 추가하거나

ContentView()
    .environment(\.myMainThemeColor, .yellow)

View속성에 대한 Extension을 추가하여 더 보기 좋게 만들 수 도 있습니다.

extension View {
    func myMainThemeColor(_ color: Color) -> some View {
        environment(\.myMainThemeColor, color)
    }
}

ContentView()
    .myMainThemeColor(.yellow)
Comments