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

[50] @Publisehd, @StateObject, @ObservedObject / MVVM

by Toughie 2023. 5. 7.

⭐️@Publisehd,  @StateObject,  @ObservedObject / MVVM⭐️

 

SwiftUI의 뷰를 많이 그려보면서 @State 프로퍼티 래퍼는 많이 써봤을 것이다.

간단하게 말하면 @State 변수에 변화가 있으면 뷰가 이를 감지하고 새로 뷰를 그린다. (리프레시된다.)

 

지금까지는 View 구조체 내부에 함수도 있고, 다양한 변수도 있고 그랬다.

규모가 작은 코드나 프로젝트에서는 상관없을 수도 있지만, 코드의 양이 방대해지면 

유지보수도 힘들고 뭐가 어디에 있는지 알기 어려워진다.

-> 코드분리의 필요성이 있다.

 

그리고 그 유명한 MVVM 아키텍쳐를 이해하기 위해서도
@Publisehd, @StateObject, @ObservedObject를 이해해야한다.

 

MVVM 아키텍쳐는

모델 - 뷰 - 뷰모델의 약자이다.

여기서는 뷰모델을 중점적으로 살펴보자.

View

UI를 정의하고 사용자 인터페이스와 상호 작용한다. 

 

Model

데이터/비즈니스 로직을 처리한다.

 

ViewModel

View와 Model의 중간다리 역할이다.

(View과 Model과 직접 상호작용하지 않도록 하기 위해서.)

why? View와 Model 사이의 의존성 낮추고, 테스트 가능성 높이기 위해

 

ViewModel은 Model의 데이터를 가져와서 View에 표시될 수 있도록 형식화하고 적절한 변환을 수행.

또한 View에서 발생하는 사용자 입력과 이벤트를 캡처하여 Model에 전달하고,

Model에서 발생하는 데이터 변경 사항을 View에 반영하는 역할도 수행한다.

ViewModel은 일반적으로 ObservableObject를 준수하며,

@Published 속성 래퍼를 사용하여 View에게 데이터 변경 사항을 알린다.

-> View에서 해당 데이터를 구독하고 새로운 데이터가 발생할 때마다 자동으로 업데이트 되도록 한다.

예시코드를 통해 적용 사례를 살펴보자.

 

커스텀 모델(데이터)

//  Created by Toughie on 2023/05/06.
//

import SwiftUI

struct Fruit: Identifiable {
    let id: String = UUID().uuidString
    let name: String
    var count: Int
}

뷰모델

//ObservableObject 프로토콜을 채택
// -> 객체의 속성이 변경되었음을 알리는 메커니즘을 의미
// 관찰은 객체의 속성 변경을 감지하는 것

final class FruitViewModel: ObservableObject {
    
   //해당 속성이 변경될 때마다 SwiftUI는 자동으로 뷰를 업데이트.
    @Published var fruitArray: [Fruit] = []
    @Published var isLoading: Bool = false
    
    //초기화 시점에 실행
    init() {
        getFruits()
    }
    
    func getFruits() {
        let fruit1 = Fruit(name: "Apple", count: 1)
        let fruit2 = Fruit(name: "Banana", count: 3)
        let fruit3 = Fruit(name: "Kiwi", count: 9)
        
        isLoading = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
            let tempFruits = [fruit1, fruit2, fruit3]
            for fruit in tempFruits {
                self.fruitArray.append(fruit)
                self.isLoading = false
            }
        }
    }
    
    func deleteFruit(index: IndexSet) {
            fruitArray.remove(atOffsets: index)
    }
}

그럼 이런 의문이 들 수 있다. Observable 프로토콜을 채택하면 내부 속성들의 변화를 감지한다는데
만약 @Published 속성 래퍼가 없다면 View가 업데이트 되지 않는건가? -> 그렇다.

 

ObservableObject 프로토콜을 채택한 객체는 objectWillChange라는 Publisher를 가지고 있다. (출판사로 비유)

 

이 Publisher는 객체 내부의 속성들이 변경될 때마다 이벤트를 발생시키고,

@Published 속성 래퍼가 사용된 속성들은 자동으로 objectWillChange를 호출한다. (신간 나왔어요!)

-> 뷰 업데이트

 

ObservableObject를 채택한 객체 내부에서 @Publisehd 속성 래퍼가 없는 속성이 변경되면,

objectWillChange 이벤트가 발생하지 않는다. -> 뷰가 업데이트 되지 않는다.

 

@Publisehd 속성 래퍼가 사용된 속성 변경 -> 해당 속성이 변경됐다는 이벤트가 objectWillChange Publisher에 전달.
-> SwiftUI가 이 이벤트를 받아 뷰를 업데이트.

 

struct ContentView: View {

    // 뷰가 리프레시 되어도 StateObject는 리프레시 되지 않음!
    // ObservableObject를 초기화 할 때 주로 사용 (처음)
    @StateObject var fruitViewModel: FruitViewModel = FruitViewModel()
    
    // 이후 다른 뷰에서 뷰모델에 접근하는 경우 (아래 예시에 나옴)
    // @ObservedObject 래퍼 사용.
    //    @ObservedObject var fruitViewModel: FruitViewModel = FruitViewModel()

    
    var body: some View {
        
        NavigationView {
            List {
                if fruitViewModel.isLoading {
                    ProgressView()
                } else {
                //모델 내부의 데이터에 접근
                    ForEach(fruitViewModel.fruitArray) { fruit in
                        HStack {
                            Text("\(fruit.count)")
                                .foregroundColor(.green)
                            Text(fruit.name)
                                .font(.headline.bold())
                        }
                    }
                    .onDelete(perform: fruitViewModel.deleteFruit)
                }
            }
            .listStyle(GroupedListStyle())
            .navigationTitle("Fruit Store")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink {
                        AnotherScreen(fruitViewModel: fruitViewModel)
                    } label: {
                        Image(systemName: "arrow.right")
                            .font(.title)
                    }
                }
            }
        }
    }
}

 

@StateObject는 SwiftUI 뷰 내에서 해당 객체를 생성하고 유지하는 데 사용된다.

이 래퍼를 사용하면 뷰가 다시 그려지더라도 해당 객체가 유지된다. 즉, 객체가 뷰의 생명주기와 함께 유지된다.
-> 객체가 생성될 때 뷰가 한 번만 업데이트 되는 경우 (주로 객체 처음으로 초기화 하는 경우)에 사용

(물론 이후에 Observable 프로토콜을 채택한 객체 내부의 @Publisehd 속성이 변경되면 뷰는 업데이트 된다.)

 

- @StateObject는 주로 해당 객체가  해당 뷰에서만 사용되는 경우에 사용됨.


@ObservedObject는 SwiftUI 뷰에서 관찰 대상 객체의 변경을 감지하여 뷰를 업데이트하는 데 사용된다.

이 래퍼를 사용하면 뷰가 관찰하는 객체의 변경을 감지하고 해당 객체가 변경될 때마다 뷰를 다시 그린다.

 

- @ObservedObject는 주로 다른 객체와 공유되는 데이터 모델이나 상태를 관리하는 경우에 사용됨.

@StateObject는 주로 해당 뷰 내에서만 사용되는 객체를 관리하고,
@ObservedObject는 다른 객체와 공유되는 객체를 관리하는 데 사용된다!

 

서브뷰

struct AnotherScreen: View {
    
    @Environment(\.presentationMode) var presentationMode
    
    //ObservedObject 프로퍼티 래퍼 사용
    // AnotherScreen 객체 초기화 할 때 FruitViewModel 타입 전달
    @ObservedObject var fruitViewModel: FruitViewModel
    
    var body: some View {
        ZStack {
            Color.green.ignoresSafeArea()
            
            VStack {
                ForEach(fruitViewModel.fruitArray) { fruit in
                    Text(fruit.name)
                        .foregroundColor(.white)
                        .font(.headline)
                        .padding(.bottom, 20)
                }
                Spacer()
                Button {
                    presentationMode.wrappedValue.dismiss()
                } label: {
                    Text("Back")
                        .foregroundColor(.white)
                        .font(.largeTitle)
                        .fontWeight(.semibold)
                }
            }
        }
    }
}

엄청 쉽게 말하면, 뷰가 뷰모델(ObservableObject)을 쳐다보고 있는데(@ObservedObject/@StateObject),
뷰모델에서 변화가 있으면(@Published) 이것을 반영해서 뷰를 새로 그린다는 것!

예시를 위한 코드라 특별한 동작은 없다.