Skip to content

SwiftUI에서 View와 Model Binding (part2: @StateObject, @ObservedObject, @Published)

강병민 (Byungmin Kang) edited this page Nov 30, 2020 · 1 revision

앞서 다룬 state와 binding은 view내에서 간단하게 상태관리를 할때는 적합하지만

우리가 실제로 쓰는 model을 이용할때는 적합하지 못합니다

이때는 @StateObject, @ObservedObject, @Published를 이용하면됩니다

그리고 이를 활용하기에 적합한 디자인패턴이 MVVM 패턴입니다

예시 앱으로 간단한 카운터 앱을 만들어보겠습니다.


먼저 Model과 ViewModel을 보여드리면

struct Counter {
    let text: String
    var count: Int
}

class CounterViewModel: ObservableObject {
    @Published var counter = Counter(text: "카운터", count: 0)
    func addCount() {
        counter.count = counter.count.advanced(by: 1)
    }
}

Counter 구조체는 여태껏 해왔듯 정말 단순한 모델입니다

CounterViewModel에서 combine을 이용하게되는데, ObservableObject 프로토콜을 체택합니다 @Published var counter 는 counter라는 변수가 바뀔때마다 알림을 보내겠다고 의미합니다

사실상

var counter = Counter(text: "카운터", count: 0) {
        didSet {
            objectWillChange.send()
        }
    }

와 똑같은 코드인데, 단순화시킨 것이 @Published property wrapper인것 같아요

이제는 해당 ViewModel을 가지는 View를 보겠습니다.

struct CountersView: View {
    @State private var showModal = false
    @StateObject var viewModel = CounterViewModel()
    
    var body: some View {
        
        VStack {
            Text("\(viewModel.counter.text)가 눌린 횟수 : \(viewModel.counter.count)")
            Button("Show Modal") {
                self.showModal.toggle()
            }.sheet(isPresented: $showModal) {
                ModalView(showModal: self.$showModal, viewModel: viewModel)
            }
        }
    }
}

struct ModalView: View {
    @Binding var showModal: Bool
    @ObservedObject var viewModel: CounterViewModel
    
    var body: some View {
        VStack(spacing: 20) {
            Text("\(viewModel.counter.text)가 눌린 횟수 : \(viewModel.counter.count)")
                .padding()
            Button("카운터 횟수 늘리기") {
                viewModel.addCount()
            }
            Button("Dismiss") {
                showModal.toggle()
            }
        }
    }
}

@StateObject var viewModel = CounterViewModel() 부분은 View의 state가 viewModel에 의존적이다를 말해줍니다. viewModel 에서 업데이트가 나타나면 SwiftUI가 그것을 알아차리고 view를 다시 만듭니다.

마찬가지로 ModalView에도 viewModel을 갖고있습니다

흐름을 생각하면

  • ModalView에서 "카운터 횟수 늘리기"버튼을 누르면 view model에게 model을 업데이트하게 함

  • model인 @Published var counter가 업데이트가 되면 viewmodel이

  • CountersView는 @StateObject, ModalView는 @ObservedObject에게 알려줌

글면 @StateObject와 @ObservedObject는 뭐가 다를까요?

여러번 얘기 나온 "Source of truth"입니다. view에 @StateObject이 있으면 해당 객체의 주인이라는 뜻이고

@ObservedObject는 그 주인이 외부에있다 라는 뜻입니다.

@StateObject is used to store new instances of reference type data that conforms to the ObservableObject protocol. This owns its data. . @ObservedObject refers to an instance of an external class that conforms to the ObservableObject protocol. This does not own its data. .

잠깐... 그러면 showModal도 ViewModel에 넣어도 되지 않을까요?

class CounterViewModel: ObservableObject {
    **@Published var showModal = false**
    @Published var counter = Counter(text: "카운터", count: 0)
    func addCount() {
        counter.count = counter.count.advanced(by: 1)
    }
}

struct CountersView: View {
    @StateObject var viewModel = CounterViewModel()
    
    var body: some View {
        
        VStack {
            Text("\(viewModel.counter.text)가 눌린 횟수 : \(viewModel.counter.count)")
            Button("Show Modal") {
                viewModel.showModal.toggle()
            }.sheet(isPresented: $viewModel.showModal) {
                ModalView(viewModel: viewModel)
            }
        }
    }
}

struct ModalView: View {
    @ObservedObject var viewModel: CounterViewModel
    
    var body: some View {
        VStack(spacing: 20) {
            Text("\(viewModel.counter.text)가 눌린 횟수 : \(viewModel.counter.count)")
                .padding()
            Button("카운터 횟수 늘리기") {
                viewModel.addCount()
            }
            Button("Dismiss") {
                viewModel.showModal.toggle()
            }
        }
    }
}

네 됩니다.

사실 각 @State를 쓸지, @StateObject를 쓸지는 명확한 정답이 있는것 같지 않습니다.

제 생각에는 view와 관련되면 @State, 그리고 viewmodel/ model 과 관련되어있으면 @StateObject를 쓰는게 좋을 것 같네요

아직은 안 써봤는데, @EnvironmentObject 라는 친구도 있습니다.

https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views

For data that should be shared with all views in your entire app, SwiftUI gives us @EnvironmentObject. This lets us share model data anywhere it’s needed, while also ensuring that our views automatically stay updated when that data changes.

Think of @EnvironmentObject as a smarter, simpler way of using @ObservedObject on lots of views. Rather than creating some data in view A, then passing it to view B, then view C, then view D before finally using it, you can create it in view and put it into the environment so that views B, C, and D will automatically have access to it.

참고자료

https://www.hackingwithswift.com/quick-start/swiftui/all-swiftui-property-wrappers-explained-and-compared

https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-stateobject-property-wrapper

https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-observedobject-property-wrapper

https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

Clone this wiki locally