swiftUI @StateObject, @ObservedObject, @EnvironmentObject 정리
1. StateObject
- view가 인스턴스를 생성하고 소유함
- 보통 ViewModel이 해당 뷰에서만 쓰이거나, 상위뷰에서 초반에 전역으로 사용할 인스턴스 생성할때 사용
struct MyView: View {
@StateObject var viewModel = MyViewModel()
}
//상위에서 생성해서 보낼때 사용
struct TmpApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@StateObject var container: DIContainer = .init(services: Services())
var body: some Scene {
WindowGroup {
AuthenticatedView(authViewModel: .init(container: container))
.environmentObject(container)
}
}
}
2. ObservedObject
- view가 인스턴스를 참조만 함
- 상위에서 주입된 ViewModel을 하위로 전달
class CounterViewModel: ObservableObject {
@Published var count: Int = 0
func increase() {
count += 1
}
func reset() {
count = 0
}
}
// 여기서 ViewModel을 생성하고, 전달
struct MainView: View {
@StateObject var viewModel = CounterViewModel() // 인스턴스 생성
var body: some View {
VStack(spacing: 30) {
Text("MainView에서 count: \(viewModel.count)")
ChildView1(viewModel: viewModel) // 같은 인스턴스 전달
ChildView2(viewModel: viewModel)
}
.padding()
}
}
// 전달받은 뷰모델 사용
struct ChildView1: View {
@ObservedObject var viewModel: CounterViewModel
var body: some View {
VStack {
Text("ChildView1 count: \(viewModel.count)")
Button("➕ 증가") {
viewModel.increase()
}
}
}
}
// 전달받은 뷰모델 사용
struct ChildView2: View {
@ObservedObject var viewModel: CounterViewModel
var body: some View {
VStack {
Text("ChildView2 count: \(viewModel.count)")
Button("🧹 리셋") {
viewModel.reset()
}
}
}
}
3. EnvironmentObject
- 상위에서 전달되면 하위의 어떤 view도 사용가능
- ViewModel에선 사용불가(다른 과정을 거쳐야 함)
//상위에서 생성해서 보낼때 사용
struct TmpApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@StateObject var container: DIContainer = .init(services: Services())
var body: some Scene {
WindowGroup {
AuthenticatedView(authViewModel: .init(container: container))
.environmentObject(container)
}
}
}
// 이렇게 받으면 됩니다.
struct AuthenticatedView: View {
@StateObject var authViewModel: AuthenticatedViewModel
@EnvironmentObject var container: DIContainer
...
}
4. (중요, 생각보다 많이 필요)서버를 호출하는 EnvironmentObject container를 다른 ViewModel에서 사용하고 싶을때
- 최상위에서 container 주입 > 어떤 하위뷰에서든 container 사용가능, But ViewModel은 사용불가
- 하위 ViewModel에서 container변수를 선언하고, init 구현
- 해당 view에서 EnvironmentObject container를 활용하여 초기화때 ViewModel생성
//최상위에서 주입
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@StateObject var container: DIContainer = .init(services: Services())
var body: some Scene {
WindowGroup {
AuthenticatedView(authViewModel: .init(container: container))
.environmentObject(container)
}
}
}
// ViewModel에서 container사용하려고, 변수 마련
class AnalysisViewModel: ObservableObject {
private let container: DIContainer
init(container: DIContainer) {
self.container = container
}
}
// View에서 EnvironObject container를 사용하여, 초기화
struct AnalysisView: View {
@EnvironmentObject var container: DIContainer
@StateObject private var viewModel: AnalysisViewModel
init(container: DIContainer) {
_viewModel = StateObject(wrappedValue: AnalysisViewModel(container: container))
}
}
// 그런데 문제, EnvironmetObject임에도 불구하고, AnalysisView호출시 container를 주입해줘야 함
// EnvironmentObject는 View가 초기화 되고 난 후, 자동주입 > 즉 초기화때는 없음 > 그래서 넣어줘야 함
AnalysisView(container: container)
- View는 초기화 된 이후, @EnvironmentObject는 View가 body를 그릴 때 값을 자동 주입.
- 근데 우리는 init할때, container를 사용. 그래서 불편하지만, AnalysisView호출시 container 넣어줘야 함
5. 실행순서 이해하기
struct AnalysisView: View {
@StateObject private var viewModel = AnalysisViewModel()
var body: some View {
Text("상태: \(viewModel.status)")
}
}
1️⃣ 부모 뷰가 AnalysisView() 호출
↓
2️⃣ SwiftUI가 AnalysisView 인스턴스를 생성
↓
3️⃣ @StateObject로 인해 AnalysisViewModel() 실행됨
→ 여기서 ViewModel 인스턴스 최초 생성
↓
4️⃣ ViewModel은 이 View가 **소유**함 (참조 유지)
↓
5️⃣ View의 body 호출
↓
6️⃣ body 안에서 viewModel 사용 가능 (이미 준비됨)
↓
7️⃣ 이후 @Published 값 변경 시 View 자동 업데이트
struct AnalysisView: View {
@EnvironmentObject var container: DIContainer
@StateObject private var viewModel: AnalysisViewModel
init(container: DIContainer) {
_viewModel = StateObject(wrappedValue: AnalysisViewModel(container: container))
}
var body: some View {
Text("분석 중: \(viewModel.status)")
}
}
1️⃣AnalysisView(container: ...)
↓
2️⃣init(container:) 실행 → ViewModel 생성됨 (@StateObject)
↓
3️⃣SwiftUI가 body 호출 → @EnvironmentObject 자동 주입됨
↓
4️⃣View 그려짐
↓
6️⃣ViewModel 값 바뀜 (@Published) → View 자동 리렌더링
6. 왜 이렇게 사용하는가?
@StateObject private var viewModel = AnalysisViewModel(container: container)
// 이렇게 사용안하고
init(container: DIContainer) {
_viewModel = StateObject(wrappedValue: AnalysisViewModel(container: container))
}
// 이렇게 사용하는 이유
View의 순서를 제대로 이해해야 한다.
1️⃣속성을 선언만 한다. 이런 값이 들어올꺼야(예: let container: DIContainer)
- 이때 값을 대입하는 것은 불가 > @StateObject private var viewModel = AnalysisViewModel(container: container) 불가!!
↓
2️⃣init() 하는 시점에 다른 외부값을 사용할수 있다
- 외부의존성 사용
- ViewModel 생성
↓
3️⃣View body가 호출됨(화면 그려짐)
- viewModel.status등 사용 가능
↓
4️⃣ViewModel의 상태(@Published) 변경 시, View가 자동으로 다시 렌더링
7. 실무에서 추가 팁
DIContainer를 ViewModelFactory처럼 만들기
class DIContainer: ObservableObject {
let services: Services
func makeAnalysisViewModel() -> AnalysisViewModel {
AnalysisViewModel(container: self)
}
}
@EnvironmentObject var container: DIContainer
@StateObject var viewModel: AnalysisViewModel
init(container: DIContainer) {
_viewModel = StateObject(wrappedValue: container.makeAnalysisViewModel())
}