앱 전체에 걸쳐 단 1개만 존재하는 오브젝트를 싱글톤 오브젝트라고 불렀습니다. EnvironmentObject도 비슷한 개념으로 보면 좋습니다. 상위 View에서 한번 인스턴스를 만들면 하위 View에서 동일한 인스턴스를 사용할 수 있습니다.
User를 관리하는 객체를 EnvironmentObject로 사용하면 좋습니다. (앱의 어디서든 로그인 여부, 사용자 이름을 참조할 수 있습니다.)
import SwiftUI
//✅ 선언하는 부분은 일반적인 ObservableObject와 동일합니다.
class MyInfo: ObservableObject {
@Published var name = "Moon"
@Published var photo = "person"
var token = ""
}
struct HomeView: View {
@EnvironmentObject var myInfo: MyInfo
//👉 싱글톤이기 때문에 인스턴스를 만들지 않고 타입만 선언합니다.! (뷰의 메모리에는 올리지 않습니다.)
var body: some View {
VStack {
Text("name: \(myInfo.name)")
.padding()
Image(systemName: myInfo.photo)
}
}
}
struct ContentView: View {
var body: some View {
HomeView()
.environmentObject(MyInfo()) //👉 여기서 처음 인스턴스를 만든다.
}
}
아래 예시는 ContentView > HomeView > DetailView의 구조를 가집니다. ContentView에서 주입한 EnvironmentObject는 중간에 위치한 View인 HomeView에서 사용하지 않더라도 그 하위 View인 DetailView에서 사용할 수 있습니다.
마치 어디서나 자유롭게 접근할 수 있는 싱글톤 오브젝트와 마찬가지로 EnvironmentObject를 주입한 View의 하위 View에서는 어디서든 자유롭게 접근할 수 있습니다.
struct ContentView: View {
//✅ 가장 상위 View에서 일단 주입만 하면!
var body: some View {
HomeView()
.environmentObject(MyInfo())
}
}
struct HomeView: View {
//🚫 중간 View에서 EnvironmentObject 사용하지 않지 않더라도
var body: some View {
NavigationView(content: {
NavigationLink(destination: {
DetailView()
}, label: {
Text("To Info Detail")
})
})
}
}
struct DetailView: View {
//👉 하위 View 어디서든 선언하고 이용할 수 있음.
@EnvironmentObject var myInfo: MyInfo
var body: some View {
VStack {
Text(myInfo.name)
Image(systemName: myInfo.photo)
}
}
}
뷰 위계가 A → B → C → D 일 때 A, D에서만 필요한 경우에는 B, C는 데이터를 가지고 있을 필요가 없습니다. 이 경우 A에 EnvironmentObject를 주입하면 D에서는 상위 뷰인 B, C에 없는 데이터라도 중간에 언제든지 접근할 수 있습니다.
최상위 View에서 인스턴스를 한번만 생성하면 그 아래에서는 그냥 쓰면 됩니다. 새로운 인스턴스를 또 만들면 안됩니다!
어거지로 MyInfo의 인스턴스를 하나 더 만들어 봅시다. 이 경우에는 두 개의 별도의 인스턴스가 생기는 셈입니다. 따라서 HomeView에 있는 myInfo와 DetailView에 있는 myInfo는 완전히 별도의 인스턴스입니다. 이름을 바꾸는 버튼을 만들어서 이름을 바꾸어도 상위 View에 전혀 반영되지 않습니다.
struct DetailView: View {
@ObservedObject var myInfo = MyInfo()
var body: some View {
VStack {
Text(myInfo.name)
Image(systemName: myInfo.photo)
Button {
myInfo.name = "Lee"
} label: {
Text("Change Name")
}
}
}
}
App의 body에서 주입하면 앱의 모든 View에서 사용할 수 있습니다.
import SwiftUI
@main
struct swiftUI_practiceApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(MyInfo())
}
}
}
EnvironmentObject는 여러개 주입할 수 있습니다. 필요한 하위 View에서 같은 타입으로 @Environment 변수를 달아서 선언하면 됩니다.
import SwiftUI
@main
struct swiftUI_practiceApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(MyInfo())
.environmentObject(MyLocation())
}
}
}
아래 예시는 로그인이 되어있지 않다면 LogInView를 보여주고 로그인이 되어 있다면 MainTabView를 보여주는 아주 단순한 코드입니다. 이 화면 만을 위해서는 굳이 EnvironmentObject를 쓸 필요가 없어 보이기도 합니다.
⭐️ 하지만 이 코드가 진가를 발휘하는 순간은 “로그아웃”을 하는 순간입니다.
사용자가 로그아웃 시키는 버튼은 MainTabView의 하위 View 어딘가에 있을 것입니다. 아주 깊숙히 있을 수도 있지요. 하지만 어떤 View에서든지 간에 로그아웃을 버튼을 누르는 순간 viewModel.user는 nil이 될 것이고 그 순간에 @Publish로 선언된 변수이기 때문에 연결된 View를 Re-render합니다.
따라서 ContentView에서는 viewModel.user를 사용한 분기문이 다시 실행되고 LogInView로 돌아오게 되는 것이지요.
만약에 EnvironmentObject가 없었다면 로그아웃을 실행한 이후에 뷰 계층을 거슬러 올라가서 다시 LogInView를 보여주는 코드를 작성해야할지도 모릅니다. 그런 수고를 Environment Object는 아래와 같은 간단한 방식으로 덜어줍니다.
class AuthViewModel: ObservableObject {
@Published var user: User
// ... 중략 ...
func logout() {
self.user = nil
}
}
struct ContentView: View {
@EnvironmentObject var viewModel: AuthViewModel
var body: some View {
Group {
if viewModel.user == nil { //👉 로그인이 안되어 있다면
LogInView()
} else { //👉 로그인이 되어 있다면
MainTabView()
}
}
}
}