728x90
[Protocol] ObservableObject
- Class 전용 프로토콜로, 데이터가 변경되면 Observer에게 알림을 보내는 타입
- iOS 17.0부터는 Observable() 매크로 사용이 권장됨
ObjectWillChange
- ObservableObjectPublisher타입으로, 객체가 수정되기 전에 전달하는 publisher이다.
- objectWillChange.send() 메서드를 사용해 변경사항을 subcriber에게 전달한다.
- 변경사항이 있을 때마다 objectWillChagne.send() 를 해야한다면, 몹시 불편할 것 같다.
- 이를 보완하는 방법으로 @Publshed 라는 property wrapper가 있다.
import SwiftUI
class TestViewModel: ObservableObject {
var count = 0
func run() {
count += 1
objectWillChange.send() // 변경사항이 있음을 전달함.
}
}
struct TestView: View {
@ObservedObject var testViewModel = TestViewModel()
var body: some View {
Button {
testViewModel.run() // 변경사항이 있음을 전달받아 View를 다시 그림.
} label: {
Text("\(testViewModel.count)")
}
}
}
@Published
- Class 전용 property wrappers로, @Published로 표시된 property를 publish하는 타입
- @Published로 선언된 property값이 변경되면 property의 willSet에 퍼블리싱이 발생하며, 해당 객체에 종속된 모든 View를 업데이트한다. 또한 자식뷰에서도 property값을 변경할 수 있으며, 이 값을 다른 Observer에게 전파할 수 있다.
// Published
class Weather: ObservableObject {
@Published var temperature: Double = 0
}
@ObservedObject, @StateObject
- ObservableObject 프로토콜을 따르는 객체를 필요로 하는 Property wrapper로, ObservableObject 프로토콜은 객체의 값이 변경되기 전에 알려주는 publisher이다.
- ObservedObject와 StateObject 둘 다 ObservableObject 프로토콜을 따른다.
@ObservedObject
- ObservableObject를 구독하고, ObservableObject가 변경될 때마다 view를 업데이트하는 기능을 가지고 있다.
final class MyViewModel: ObservableObject {
@Published var count: Int = 0
func increment() {
count += 1
}
}
struct MyView: View {
@ObservedObject var viewModel = MyViewModel() // 관찰가능한 객체
var body: some View {
Button {
viewModel.increment()
} label: {
Text("\\(viewModel.count)") // 버튼을 누르면 count 증가
}
}
}
- 하지만, 작은 문제가 있었으니,, View가 새롭게 그려질 때, 데이터가 초기화되는 문제가 있다.
- 부모뷰의 State가 변경되어 View가 새롭게 렌더링 되면서 데이터가 초기화되는 모습
struct ParentView: View { // 부모뷰
@State private var count = 0
var body: some View {
Button {
count += 1
} label: {
// 버튼을 누르면 State값 변경. View 렌더링
Text("State: \(count)")
}
MyView()
}
}
final class MyViewModel: ObservableObject {
@Published var count: Int = 0
func increment() {
count += 1
}
}
struct MyView: View { // 자식뷰
@ObservedObject var viewModel = MyViewModel()
var body: some View {
Button {
viewModel.increment()
} label: {
Text("ObservedObject: \(viewModel.count)")
}
}
}
- 이런 문제를 쉽게 해결하기 위해 @StateObject가 등장하게 되었다.
@StateObject
- ObservedObject과 동일하게, ObservableObject를 구독하고, ObservableObject가 변경될 때마다 view를 업데이트하는 기능을 가지고 있다.
- 하지만 StateObject는 생성 당시 1회만 init이 되고 @State의 속성을 따라 부모 뷰가 새롭게 그려지더라도, StateObject가 구독하고 있는 ObservableObject는 Single Source of Truth(단일 원본 데이터)를 참조하고 있기 때문에, view의 LifeCycle에 의존하지 않는다.
- 따라서 아래처럼 동작한다.
- 부모뷰의 State가 변경되어 View가 새롭게 렌더링 되어도, 데이터가 초기화되지 않는 모습.
struct ParentView: View {
@State private var count = 0
var body: some View {
Button {
count += 1
} label: {
Text("State: \(count)")
}
MyView()
}
}
final class MyViewModel: ObservableObject {
@Published var count: Int = 0
func increment() {
count += 1
}
}
struct MyView: View {
@StateObject private var viewModel = MyViewModel() // 수정
var body: some View {
Button {
viewModel.increment()
} label: {
Text("StateObject: \(viewModel.count)")
}
}
}
- (의문) 근데 이렇게 동작하면, StateObject는 메모리에서 해제가 안 되는 게 아닐까? → 메모리 누수 일어나는거 아니야???
- 실제로 init은 view가 처음 그려질 때 한 번 호출되고, deinit이 동작하지 않는 것으로 보아, 메모리에서 해제되려면 MyView가 메모리에서 해제되어야 하는 것처럼 보였다. ( State로 뷰가 Re-Rendering 되는 것과는 별개로.. )
- 사실, StateObject는 View의 identity를 참조해 LifeCycle을 유지하는데, 이는 View의 id() modifier를 사용하면 알 수 있다. identity를 수정하면, ObservedObject와 동일한 동작이 이뤄진다.
- 왜냐하면, id()가 변경되면 view가 새롭게 그려지면서 id가 변경되고, StateObject가 이를 비교해 다른 View(id)라고 인식하여 재 초기화를 하게 된다. 이러한 동작은 State에서도 주의해야 했던 성능과 비용, 사이드이펙트가 발생시킬 수 있다는 문제가 있다.
- 당연하게도 State의 속성을 가진 State, GestureState, FocusState로도 초기화될 수 있기 때문에 주의해야 한다.
struct ParentView: View {
@State private var count = 0
@State private var identity = 0
var body: some View {
Button {
count += 1
identity += 1
} label: {
Text("State: \\(count)")
}
MyView()
.id("\\(identity)")
}
}
final class MyViewModel: ObservableObject {
@Published var count: Int = 0
init() {
print("메모리 할당")
}
func increment() {
count += 1
}
deinit {
print("메모리 해제")
}
}
struct MyView: View {...}
// identity 변경 시
// 메모리 할당
// 메모리 해제
// 메모리 할당
// 메모리 해제
P.S
Single Source of Truth(SSoT, 단일 데이터 원본)
SwiftUI는 Property's Storage라는 외부 데이터공간을 관리하고 있다.
Property's Storage에 있는 데이터가 SSoT이다.
반응형
'🍎 iOS > DevNote' 카테고리의 다른 글
[Combine] Combine 개념 및 등장배경 (0) | 2024.04.28 |
---|---|
[Swift] NSCache 이해하기 (0) | 2024.03.17 |
[SwiftUI] CustomTabView 구현 (0) | 2024.03.04 |
[Github] 우리는 왜 Squash & Merge와 No Fast-forward Merge 방식을 채택했을까? (2) | 2024.02.04 |
[Swift] @main (0) | 2023.07.30 |
댓글