부모 View에 카운터, 자식 View에도 카운터가 있는 View를 만들어 보았습니다.
부모 View는 @State
로 number 값을 변경하여 View를 업데이트합니다.
자식 View는 @StateObject
로 ViewModel을 가지고 있고, viewModel의 number 값을 변경하여 View를 업데이트합니다.
그런데, 부모 View의 number값이 변경될 때마다 자식 View의 ViewModel이 매번 init()
이 됩니다.
그 이유가 무엇일까요?
import SwiftUI
class ViewModel: ObservableObject {
@Published private(set) var number: Int = 0
init() {
print("viewModel initialized")
}
func upCount() {
number += 1
}
}
struct ContentView: View {
@State private var count: Int = 0
var body: some View {
VStack {
ChildView()
Button("부모 뷰 카운터 = \(count)") {
count += 1
}
}
.padding()
.border(.red)
}
}
struct ChildView: View {
@StateObject var viewModel: ViewModel
init() {
let viewModel = ViewModel()
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
VStack {
Button("자식 뷰 카운터 = \(viewModel.number)") {
viewModel.upCount()
}
}
.padding()
.border(.blue)
}
}
struct ChildView: View {
@StateObject var viewModel: ViewModel
init() {
let viewModel = ViewModel()
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
VStack {
Button("자식 뷰 카운터 = \(viewModel.number)") {
viewModel.upCount()
}
}
.padding()
.border(.blue)
}
}
위 상황에서 @StateObject
인 ViewModel을 생성할 때 자식 View의 init()에서 생성하고 있습니다.
일반적으로 @StateObject var viewModel = ViewModel()
처럼 선언하지만,
ViewModel 내부에 특정 값을 초기화해야 할 경우에는 View의 init()
에서 ViewModel을 생성하게 됩니다.
하지만, 여기서 @StateObject
의 초기화 과정을 명확하게 이해하지 못하면 문제가 발생할 수 있습니다.
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
@frozen @propertyWrapper public struct StateObject<ObjectType> : DynamicProperty where ObjectType : ObservableObject {
@inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
...
}
위 코드는 SwiftUI에서 @StateObject
가 구현된 부분입니다.
init()
함수에는 @autoclosure
가 적용되어 있습니다.
이 @autoclosure
는 wrappedValue에 값을 할당하며, View의 lifecycle 동안 오직 한 번만 호출됩니다.
init() {
let viewModel = ViewModel()
_viewModel = StateObject(wrappedValue: viewModel)
}
하지만, View의 init()
함수는 자주 호출될 수 있습니다.
때문에 _viewModel = StateObject(wrappedValue: viewModel)
구문은 한 번만 호출되지만,
let viewModel = ViewModel()
구문은 부모 View의 @State
값 변화에 따라 지속적으로 실행됩니다.
이 문제를 해결하는 방법은 두 가지가 있습니다.
@StateObject(wrappedValue:)
값을 _viewModel에 직접 대입하는 방법 init() {
_viewModel = StateObject(wrappedValue: ViewModel())
}
@StateObject(wrappedValue:)
로 ViewModel을 만든 후 _viewModel에 대입하는 방법 init() {
let viewModel = StateObject(wrappedValue: ViewModel())
_viewModel = viewModel
}
import SwiftUI
class ViewModel: ObservableObject {
@Published private(set) var number: Int = 0
init() {
print("viewModel initialized")
}
func upCount() {
number += 1
}
}
struct ContentView: View {
@State private var count: Int = 0
var body: some View {
VStack {
ChildView()
Button("부모 뷰 카운터 = \(count)") {
count += 1
}
}
.padding()
.border(.red)
}
}
struct ChildView: View {
@StateObject var viewModel: ViewModel
init() {
_viewModel = StateObject(wrappedValue: ViewModel())
}
var body: some View {
VStack {
Button("자식 뷰 카운터 = \(viewModel.number)") {
viewModel.upCount()
}
}
.padding()
.border(.blue)
}
}