안녕하세요.
이전 글에선 @State, @Binding, @Published 3가지 Property Wrapper에 대해 알아보았고,
이번엔 @ObservedObject, @StateObject에 대해 알아보겠습니다.
@ObservedObject와 @StateObject
A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
출처: 공식문서
둘은 비슷한 상황에 사용되고, 공통점이 많기 때문에 같이 정리해 보겠습니다.
@ObservedObject와 @StateObject는 ViewModel 객체와 같은 반응형 객체를 만들 때 사용되는 Property Wrapper입니다.
두 Property Wrapper를 사용하려면, 해당 객체는 아래와 같이 ObservableObject 프로토콜을 준수하는 클래스 객체여야 합니다.
class DataModel: ObservableObject {
@Published var name = "Some Name"
}
@ObservedObject와 @StateObject는 해당 객체를 SwiftUI에서 View가 관찰 가능한 객체로 만들어 주며, 데이터 변경에 따른 UI를 다시 그릴 수 있게 해줍니다.
ObservableObject 객체는 View가 변경에 감지해야 할 프로퍼티를 이전에 다뤘던 @Published Property Wrapper를 통해 선언해 줍니다.
또는, @Published로 선언하지 않고, 아래와 같이
class DataModel: ObservableObject {
var name = "Some Name"
func changeName(name: String) {
self.name = name
objectWillChange.send()
}
}
objectWillChange.send() 메서드를 호출해 ObservableObject 객체의 상태값이 변경되었다는 것을 수동으로 View에게 전달할 수 있습니다.(기본적으로 @Published 사용함.)
그럼 차이점은 무엇일까?
@ObservedObject와 @StateObject의 차이점
@ObservedObject는 아래와 같이 View 객체 내부에서 인스턴스화할 경우 한 가지 문제가 발생합니다.
@ObservedObject로 선언한 객체의 경우 해당 View가 그려질 때 같이 초기화됩니다.
코드를 통해 확인해 보면,
struct SubView: View {
@ObservedObject private var dataModel = DataModel()
var body: some View {
VStack {
Text("이름: \(dataModel.name)")
Button("랜덤 이름") {
dataModel.changeName(name: "ABC\(Int.random(in: 1...1000))")
}
}
}
}
class DataModel: ObservableObject {
@Published private(set) var name = "Some Name"
func changeName(name: String) {
self.name = name
}
}
SubView가 있고, 이름을 데이터를 관리하는 @ObservedObject로 선언된 DataModel 객체가 있습니다.
struct ContentView: View {
@State private var code: Int = 0
var body: some View {
VStack {
Text("코드: \(code)")
SubView()
Button("랜덤 코드") {
code = Int.random(in: 1...1000)
}
}
.padding()
}
}
ContentView 내부에는 위에서 만든 SubView가 있고, 코드값을 보여주는 TextView와 코드를 변경해주는 Button이 있습니다.
이름을 변경하다가 코드가 변경되면 이름값이 초기화되는 것을 확인할 수 있습니다.
위와 같은 문제를 해결하기 위해 2가지 방법이 있는데
- @ObservedObject 객체를 부모 뷰에서 주입시켜주는 방법
- @StateObject를 사용하는 방법
첫번째를 해보면,
struct ContentView: View {
@State private var code: Int = 0
@ObservedObject private var dataModel = DataModel()
var body: some View {
VStack {
Text("코드: \(code)")
SubView(dataModel: dataModel)
Button("랜덤 코드") {
code = Int.random(in: 1...1000)
}
}
.padding()
}
}
struct SubView: View {
@ObservedObject private var dataModel: DataModel
init(dataModel: DataModel) {
self.dataModel = dataModel
}
var body: some View {
VStack {
Text("이름: \(dataModel.name)")
Button("랜덤 이름") {
dataModel.changeName(name: "ABC\(Int.random(in: 1...1000))")
}
}
}
}
위와 같이 DataModel을 부모뷰로 부터 주입받도록 수정했습니다.
위와 같이 코드가 변경될 때 이름이 초기화되지 않습니다.
두 번째도 해보면,
struct SubView: View {
@StateObject private var dataModel = DataModel()
var body: some View {
VStack {
Text("이름: \(dataModel.name)")
Button("랜덤 이름") {
dataModel.changeName(name: "ABC\(Int.random(in: 1...1000))")
}
}
}
}
이번엔 @StateObject로 선언하였습니다.
똑같이 문제를 해결할 수 있었습니다.
즉, 정리해 보자면,
외부에서 주입을 통해 ObservableObject 객체를 받는다면, @ObservedObject 사용을 권장 드리고,
내부에서 인스턴스화해서 사용한다면, 안전하게 @StateObject 사용을 권장 드립니다.
여기까지 @ObservedObject와 @StateObject에 대해 알아보았습니다.
원래 2편을 기획했는데 양 조절을 실패했네요.
다음 편에서 @Environment에 대해 알아보겠습니다.
'Swift' 카테고리의 다른 글
[Swift] @Environment에 대해 알아보기(Property Wrapper 3편) (0) | 2024.01.20 |
---|---|
[Swift] @State, @Binding, @Published 대해 알아보기(Property Wrapper 1편) (0) | 2024.01.19 |
[Swift] Alamofire의 RequestInterceptor을 사용해 토큰 갱신하기 (0) | 2024.01.18 |
Combine 뽀개기 4장: Scheduler (0) | 2023.07.26 |
Combine 뽀개기 3장: Subject (0) | 2023.07.26 |