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 객체에 대한 참조를 갖는다.