Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
SwiftUI/SwiftUI(Basic)

[51] @EnvironmentObject / 뷰 계층 구조

by Toughie 2023. 5. 7.

⭐️@EnvironmentObject / 뷰 계층 구조 View Hierarchy⭐️

 

지난 시간 @StateObject, @ObservedObject를 떠올려 보자.

만약 여러 뷰에서 모델에 접근해야 하는 경우라면 위 개념들을 활용할 경우 매 뷰마다 바인딩을 해줘야 한다.

(ex. 네비게이션 뷰를 활용하는 경우 - 네비게이션 링크마다 모델에 대한 바인딩이 필요함.)

 

결국 네비게이션 뷰에서 이어지는 네비게이션 링크들은 모두 같은 뷰 계층에 속해있는데 말이다.

이런 경우에 environmentObject를 활용할 수 있다.

 

environmentObject는 여러 뷰에서 공유되는 데이터를 관리하기 위한 방법 중 하나이다.

environmentObjects는 ObservableObject 프토토콜을 준수하는 객체를 받아서
이 객체를 뷰 계층 구조의 일부분인 환경(Environment)으로 취급한다.

이 객체는 이제 하위 뷰에서 @EnvironmentObject 프로퍼티 래퍼를 사용해 접근할 수 있다.

 

즉 environmentObject를 활용하면 전역으로 공유되는 상태를 생성하는 것이고, 이 상태는 앱 전반적으로 사용가능하다.

그리고 상태가 변경될 때마다 뷰를 업데이트 한다.(상태가 변경되면 관련된 모든 하위 뷰가 업데이트 되어서 상태 관리가 유용함)

 

예시를 통해 살펴보자.

먼저 뷰모델이 있고, 네비게이션 뷰 -> 네비게이션 링크 -> 네비게이션 링크로 이루어진 예시이다.

 

뷰 모델

import SwiftUI

class EnvironmentViewModel: ObservableObject {
    
    @Published var productArray: [String] = []
    
    let colorArray: [String] =  ["silver", "spaceGray", "white", "black"]
    
    init() {
        getData()
    }
    
    func getData() {
        self.productArray.append(contentsOf: ["iPhone 14", "iPad Pro", "Apple Watch", "Macbook Pro"])
    }
}

메인 뷰

struct EnvironmentObjectPrac: View {
    //뷰모델 초기화
    @StateObject var viewModel = EnvironmentViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.productArray, id: \.self) { item in
                    NavigationLink {
                        DetailView(selecteditem: item)
                    } label: {
                        Text(item)
                    }
                }
            }
            .navigationTitle("Apple Store")
        }
        //뷰모델을 환경으로 취급한다.(하위 뷰에서 접근 가능하도록)
        //즉 NavigationView의 하위 뷰들은 뷰모델에 접근 가능
        .environmentObject(viewModel)
    }
}

디테일 뷰 (네비게이션 링크 1)

struct DetailView: View {
    
    let selecteditem: String
    
    //만약 environmentObject를 사용하지 않았다면.. 다음 뷰로 전달하기 위해 아래 코드가 필요함.
	//@ObservedObject var viewModel: EnvironmentViewModel
    //여기서는 뷰모델에 접근해서 데이터 쓰는것도 없는데 그저 전달하기 위해 존재함 -> 비효율적
    
    var body: some View {
        ZStack {
            // background
            Color.cyan.opacity(0.7).ignoresSafeArea()
            
            NavigationLink {
            	//FinalView에서는 뷰모델 접근이 필요함(environmentObject를 통할 예정)
                FinalView()
            } label: {
                Text(selecteditem)
                    .font(.headline)
                    .foregroundColor(.orange)
                    .padding()
                    .padding(.horizontal)
                    .background(Color.white)
                    .cornerRadius(30)
            }
        }
    }
}

파이널 뷰(네비게이션 링크2)

struct FinalView: View {
    
    //메인 뷰에서 navigationView에 .environment(viewModel)을 해줬기에 아래와 같이 접근 가능!
    @EnvironmentObject var viewModel: EnvironmentViewModel
    
    var body: some View {
        ZStack {
            
            LinearGradient(gradient: Gradient(colors: [Color.black.opacity(0.9),Color.gray]), startPoint: .topLeading, endPoint: .bottomTrailing).ignoresSafeArea()
            
            //foreground
            ScrollView {
                VStack(spacing: 20) {
                    Text("\"SELECT COLOR\"")
                        .font(.largeTitle)
                        .foregroundColor(.white)
                        .padding(.vertical, 20)
                    ForEach(viewModel.colorArray, id: \.self) { item in
                        Text(item)
                    }
                }
                .foregroundColor(.white)
                .font(.largeTitle)
            }
        }
    }
}

 

뷰 계층 구조

SwiftUI에서 뷰 계층구조(View Hierarchy)는 계층적으로 구성된 뷰의 모음을 의미.

뷰 계층구조는 각각의 뷰가 다른 뷰의 상위 뷰(부모 뷰) 또는 하위 뷰(자식 뷰)인 관계로 구성된다.

최상위 뷰는 앱의 루트 뷰(Root View)이며, 이 뷰를 기반으로 하위 뷰가 계속해서 추가될 수 있다.

(이러한 구조는 트리(Tree) 형태로 표현됨.)

SwiftUI에서 뷰 계층구조를 구성하는 뷰는 모두 View 프로토콜을 준수해야 한다.

View 프로토콜을 준수하는 뷰는 body 프로퍼티를 가지고 있으며, body 프로퍼티에서 뷰를 생성하고 구성한다.(계산 속성)

또한, 뷰 계층구조의 뷰간 데이터를 전달하는 방법으로는

@State, @Binding, @EnvironmentObject, @ObservedObject 등의 프로퍼티 래퍼를 사용할 수 있다.

 

뷰 계층구조에서는 각 뷰가 자체적으로 화면을 그리는 역할을 수행한다.

뷰가 생성될 때마다 해당 뷰의 body 프로퍼티가 호출되어 뷰를 생성하고,

뷰의 상태(@State 등)가 변경될 때마다 해당 뷰의 body 프로퍼티가 다시 호출되어 업데이트된다.

(SwiftUI는 선언형 프로그래밍 방식을 채택했기에 필요한 뷰만 효율적으로 업데이트 됨.)

 

cf 명령형 프로그래밍 방식(UIKit에서 등)에서는 뷰의 상태가 변경되면 개발자가 직접 뷰를 업데이트 해줘야함.

but SwiftUI에서는 상태와 뷰의 렌더링이 분리되어 있어서 뷰의 상태가 변경되면 SwiftUI가 자동으로 뷰를 업데이트함.

 

프로퍼티 래퍼 정리

@State

뷰 내부에서 사용하는 데이터를 저장하고 관리. 값이 변경되면 해당 뷰가 다시 렌더링 된다.


@Binding

하위 뷰에서 상위 뷰의 값을 읽어오고, 값을 변경할 수 있다. Binding을 사용하면 두 뷰가 동기화된다.


@EnvironmentObject

어디서든 접근 가능한 전역적인 데이터를 저장. 뷰 계층구조에서 상위 뷰에서 설정된 객체를 하위 뷰에서 사용할 수 있다.


@ObservedObject

뷰 내부에서 생성된 객체를 관찰하고, 값이 변경(@Published)되면 해당 뷰가 다시 렌더링 된다.

뷰 내부에서 생성된 객체를 다른 뷰에서 사용할 수 있다.


이외에도 @Environment, @GestureState, @StateObject, @FetchRequest, @AppStorage 등 다양한 프로퍼티 래퍼가 있다.