본문 바로가기

카테고리 없음

[SwiftUI] 데이터 흐름 - 2 PropertyWrapper

SwiftUI에서는 데이터를 다루는데 다음과 같은 도구들이 사용된다.

 

  • @State
  • @Binding
  • ObservableObject
  • @ObservedObject
  • @Published
  • @EnvironmentObject
  • @GestureState
  • ....

 


@State

View 자신의 UI 상태를 저장하기 위한 프로퍼티, 원천 자료.

해당 View가 소유하고 관리한다는 개념을 명시적으로 나타내기 위해 항상 private를 사용하는 것이 좋다.

 

궁금한게

SuperView에서 @State로 선언한 변수가 ChildView들에게 계속 전달되면

@State로 선언한 변수가 바뀔 때 ChildView2, ChildView3.. 들도 다 업데이트 될까?

 

struct SuperView: View {
    
    @State private var name = "홍길동"

    var body: some View {
        VStack {
            ChildView(name: name)
            ButtonView(name: $name)
        }
        
    }
}

struct ChildView: View {
    
    var name: String
    
    var body: some View {
        VStack {
            Text("ChildView의 name = \(name)")
            ChildView2(name: name)
        }
    }
}

struct ChildView2: View {
    
    var name: String
    
    var body: some View {
        Text("ChildView2의 name = \(name)")
    }
}

 

답: 변경된다.

 

@Binding

상위 뷰가 가진 상태를 하위 뷰에서 사용하고 수정할 수 있게 하는 프로퍼티, 파생자료

 

@State 프로퍼티를 @Binding 프로퍼티에 넘겨주려면 $달러 기호 접두어를 사용해야 한다.

$ 기호와 @State 프로퍼티를 함께 사용하면 내부적으로 projectedValue 라는 프로퍼티를 이용하게 되는데, 이 타입이 Binding 타입이기에 Binding 프로퍼티에 @State 프로퍼티를 넘겨줄 수 있다.

 

struct State_BindingView: View {
    
    @State private var isFavorite = true
    
    var body: some View {
        Toggle(isOn: $isFavorite) {
            Text("isFavorite = \(isFavorite.description)")
        }
    }
}

 


ObservableObject와 @ObservedObject

@State가 뷰 자신이 상태를 저장하고 다루기 위한 원천자료로 이용되었다면,

뷰 외부의 모델이 가진 원천 자료를 다루기 위한 도구도 제공된다.

 

그중에서도 값 타입(value)이 아닌 참조 타입(refrence)를 사용하는 경우에 ObservableObject가 사용된다.

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ObservableObject : AnyObject {

    /// The type of publisher that emits before the object has changed.
    associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never

    /// A publisher that emits before the object has changed.
    var objectWillChange: Self.ObjectWillChangePublisher { get }
}

 

외부의 class 모델에 ObservableObject 프로토콜을 준수시키고

View에서는 @ObservedObject로 해당 모델의 객체를 갖고 있으면

이 View는 저 모델에게 의존성을 갖는다는 것을 뜻한다.

 

ObservableObject 안의 모든 변수가 변경될 때 마다 View를 다시 그릴수는 없으니까

@Published 프로퍼티 래퍼를 사용한다.

 

class User: ObservableObject {
    var name: String = "홍길동"
    @Published var score = 0
}

struct ContentView: View {
    
    @ObservedObject var user: User
    
    var body: some View {
        VStack(spacing: 30) {
            Text(user.name)
            
            Button {
                self.user.score += 1
            } label: {
                Text(user.score.description)
            }

        }
    }
}

#Preview {
    ContentView(user: User())
}

 

하지만 @Published를 붙인 프로퍼티는 변경될 때 마다 View가 업데이트 된다.

프로퍼티의 변경 시점에 즉시 View를 갱신해라!가 아닌 본인이 직접 정하여 알리고 싶다면?

ObservableObject 프로토콜에 있는 objectWillChange 프로퍼티를 사용한다.

class User: ObservableObject {
    var name: String = "홍길동"

    /// Ver1
    @Published var score = 0
    
    /// Ver2
    var score = 0 {
        willSet {
            objectWillChange.send()
        }
    }
    
    let objectWillChange = ObjectWillChangePublisher()
}

 

@Published는 ObjectWillChangePublisher가 send 메서드를 호출하는 코드를 간소화 시킨 PropertyWrapper 일 뿐이다!

 

ObservableObject는 뷰 자신이 갖고 통제하는 데이터가 아닌 외부의 모델을 참조하는것이기 때문에,

ParentView -> ChidlView 전달될 때 ChildView는 그 변수를 @Binding으로 갖고 있는 경우가 많다.

 


@EnvironmentObject

 

@ObservedObject가 모델에 대한 직접적인 의존성을 만드는데 사용되었다면,

@EnvironmentObject는 간접적인 의존성을 만드는데 사용된다.

 

 

Model과 View3개가 있을 때,

@ObservedObject를 사용하면

Model -> (@ObservedObject) -> View1 -> (@Binding) -> View 2-> (@Binding) -> View3

 

이런식으로 체이닝 방식으로 View들간에 Binding으로 객체를 넘겨줘야 한다.

 

@EnvironmentObject를 사용하면

 

Model -> (.environmentObject()) -> Environment

                 View1                   View2               View3

 

.environmentObject Modifier를 통해 특정 View에 대한 환경 요소로 ObservableObject 모델을 등록하면,

그 View를 포함한 모든 자식 View에서 @EnvironmentObject 프로퍼티 래퍼를 통해 동일한 모델에 의존성이 생긴다.

 

struct PreviewView: View {
    var body: some View {
        ParentView()
            .environmentObject(User())
    }
}

struct ParentView: View {
    var body: some View {
        VStack {
            ChildView()
            ChildView2()
        }
        
    }
}

struct ChildView: View {
    
    @EnvironmentObject var user: User
    
    var body: some View {
        Text(user.name.description)
    }
}

struct ChildView2: View {
    
    @EnvironmentObject var user: User
    
    var body: some View {
        Text(user.name.description)
    }
}


#Preview {
    PreviewView()
}

 

최상위에서 ParentView에 전달해준 environmentObject로 ChildView, ChildView2에서 동일한 User 객체에 대한 참조를 갖는다.