ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SwiftUI] MVI Design Pattern 이란?
    Swift/SwiftUI 2022. 12. 26. 15:15

    MVVM 을 사용하다 보니 이게 SwiftUI 에서 최선이 아닐 탠데 라는 고민을 하게 되었습니다..
    그 고민을 하게 된 원인은 👇👇
     
    1. ViewModel 에서 너무 많은 일을 하고 있다. - Action 와 Property 를 모두 관리하려고 하다 보니 흡사 UIKit 에서 MVC (Massive View Controller) 와 같이 ViewModel 이 Massive 해지는 경험을 하게 되었습니다.
    2. Data 의 흐름이 관리가 되지 않는다. - 1번째 이유가 겹치는 이야기인데 ViewModel 에서 Property의 값을 변경하기도 하고 참조하기도 하고 다양한 일을 하다보니 점점 Data Flow 가 어떻게 흐르는지 추적하기가 어려워 졌습니다.
     
    그래서 고민하게 되었고 SwiftUI 에서 각광받고 있는 MVI 패턴에 대해 공부하게 되었습니다.
    MVI 패턴은 이미 안드로이드에서 오래전부터 사용하고 있던 디자인 패턴이고 SwiftUI 와 잘 맞는 다고 소문이 나서 몇몇 분들이 열심히 쓰고 계신 디자인 패턴입니다. 그럼 MVI 패턴이 뭔지 알아 보도록 하겠습니다 
     

    MVI

    Model - 화면에 보여질 Data 를 가지고 있습니다.
    View - Model 을 참조하여 Data 를 보여줍니다.
    Intent - 유저의 Action에 대한 핸들링, 라이프 사이클에 따른 실행등을 담당합니다.
    Container - 그리고 코드 레벨에서 보겠지만 Intent 와 Model 은 컨테이너에 담겨 View 에서 instance 를 들고 있습니다
     

    Container

    final class MVIContainer<Intent, Model>: ObservableObject {
    
        // 2
        let intent: Intent
        let model: Model
    
        private var cancellable: Set<AnyCancellable> = []
    
        init(intent: Intent, model: Model, modelChangePublisher: ObjectWillChangePublisher) {
            self.intent = intent
            self.model = model
    
            // 3
            modelChangePublisher
                .receive(on: RunLoop.main)
                .sink(receiveValue: objectWillChange.send)
                .store(in: &cancellable)
        }
    }

    View 에서 들고 있을 Container 입니다.
     

    View

    extension ListView {
    
        static func build() -> some View {
            let model = ListModel()
            let intent = ListIntent(model: model)
    
            let container = MVIContainer(
                intent: intent,
                model: model as ListModelStatePotocol,
                modelChangePublisher: model.objectWillChange)
    
            return ListView(container: container)
        }
    }

    함수의 리턴값을 통해 View 를 만들어 보여줍니다
    Intent 는 Model 에 접근해 값을 바꿔줘야하기 때문에 Model 인스턴스를 들고 있습니다

    struct ListView: View {
    
        // 1
        @StateObject private var container: MVIContainer<ListIntent, ListModelStatePotocol>
    
        var body: some View {
            // 2
            Text(container.model.text)
                .padding()
                .onAppear(perform: {
                    // 3
                    self.container.intent.viewOnAppear()
                })
        }
    }

    그리고 View 에서는 container 를 StateObject 로 들고 있습니다
    ObservedObject 가 아닌 이유는 뷰를 다시 그릴때 마다 container 를 새롭게 init 하지 않기 위함입니다.
     

    Intent

    final class ListIntent {
    
        // 1
        private weak var model: ListModelActionsProtocol?
    
        init(model: ListModelActionsProtocol) {
            self.model = model
        }
    
        func viewOnAppear() {
            let number = Int.random(in: 0 ..< 100)
    
            // 2
            model?.parse(number: number)
        }
    }

    Intent 는 View 에서 호출된 유저의 액션에 따라 Model 에 접근해 값을 변경하고 View 에서 참조할 Data 를 변경합니다.
    하지만 View 에서 Intent 의 Model 에 바로 접근할수 없도록 private 으로 선언합니다.
     

    Model

    // 1
    protocol ListModelStateProtocol {
        var text: String { get }
    }
    
    // 2
    protocol ListModelActionsProtocol: AnyObject {
        func parse(number: Int)
    r

    1. ListModelStateProtocol 에서는 View 에서 참조할 Property 들에 대한 내용만 들고 있습니다
    2. ListModelActionsProtocol 에서 Intent 에서 Model 의 값을 변경할때 호출할 함수들에 대한 내용을 들고 있습니다.

    // 1
    final class ListModel: ObservableObject, ListModelStatePotocol {
        @Published var text: String = ""
    }
    
    // 2
    extension ListModel: ListModelActionsProtocol {
    
        func parse(number: Int) {
            text = "Random number: " + String(number)
        }
    }

     

    참고

    SwiftUI and MVI

    UIKit first appeared in iOS 2, and it is still here. Eventually we got to know it well and learned how to work with it. We have found many…

    medium.com

     

Designed by Tistory.