1⃣ MVI 등장 배경 / 필요성
SwiftUI의 MVVM ..?
1. MVVM에서 ViewModel의 역할
- View의 이벤트와 작업을 바인딩합니다.
- 뷰(VC)에서 이벤트를 수신하면 이벤트 처리 및 결과로 취해야 할 조치(사용자 인터페이스 새로고침 등)를 뷰로 전달합니다.
SwiftUI 자체에서 제공하는 바인딩을 통해 이벤트를 처리하면 비교적 쉽게 처리할 수 있습니다.
MVVM에서는 ViewModel의 역할이 크게 줄어듭니다.
2. MVVM의 양방향 데이터 흐름
MVVM은 아래와 같이 양방향 데이터 흐름을 제공합니다.

하지만 SwiftUI는 단방향 데이터 흐름을 기반으로 하기 때문에 양방향 데이터 흐름인 MVVM은 조금 부적절해 보였습니다.
2⃣ MVI
MVI(Model – View – Intent)는 MVC, MVP 및 MVVM과 동일한 제품군의 아키텍처 패턴입니다.
단방향 데이터 흐름을 제공합니다.

- 모델
- 앱의 상태를 나타내는 데이터 모델
- 인텐트에서 데이터를 수신하고 UI에 표시할 준비를 하며 현재 상태(최신 데이터)를 갖습니다.
- 뷰의 현재 상태를 항상 유지(상태 == 모델)
- 보기 보기
- 보다
- 배포된 모델로 UI 배포(상태)
- 의도 참조
- 의도
- 비즈니스 로직에서 이벤트 수신 보기 및 실행
- 모델 보기
모델이 SwiftUI의 구조체 유형 보기를 참조할 수 있나요?
모델이 업데이트되면 뷰에 반영하기 위해 뷰를 참조해야 합니다.
SwiftUI의 뷰는 구조체 유형이기 때문에 참조할 수 없습니다.
그래서 컨테이너개념이 나옵니다.
컨테이너
– 인텐트와 모델에 대한 레퍼런스 관리 및 접근성 제공
– View 생성 시 컨테이너를 포함하여 생성
3⃣ MVI 예시
아래 예제보다 간단한 MVI 예제는 다음과 같습니다. 지름길아래에서 확인하실 수 있습니다
샘플 앱 내의 기능
“개”에 대한 키워드 검색 결과에서 이미지를 가져오는 기능
- 최대 노출 10프레임


화신
MVI는 크게 Model → Intent → View(컨테이너) 순으로 구현됩니다.
1. ModelStateProtocol, ModelActionsProtocol, 모델
– 뷰에 표시할 상태와 인텐트에서 모델이 취할 조치를 정의합니다.2. 의도 프로토콜, 의도
– 뷰에서 이벤트 발생시 실행할 로직 구현3. MVI컨테이너 보기
– 모델 및 의도를 포함하는 컨테이너 생성 및 연결
1. 콘텐츠 상태
- ContentState에 따라 보기에서 보이는 콘텐츠 분리
enum ContentState {
case loading
case content(images: (ImageItem))
case error
}
두 번째 모델상태규약
- 뷰에 표시할 모든 모델(상태) 선언
- 텍스트, 이미지, 분기 조건 등을 표시합니다.
- 예) navigationTitle, loadingText, contentState
- 모델로 뷰를 그릴 때 액션과 히스토리를 분리하여 내부 로직을 숨기고 속성에 대한 접근만 허용합니다.
protocol SearchModelStateProtocol {
var contentState: ContentState { get }
var navigationTitle: String { get }
var loadingText: String { get }
var errorText: String { get }
}
3. 모델행위규약
- 모델에서 값을 변경하는 논리 함수 정의
- 모델 내 상태 값 변경, UI 적용을 위한 데이터 처리 등
- 인텐트를 통해서만 호출
protocol SearchModelActionsProtocol: AnyObject {
func displayLoading()
func updateImage(images: (ImageItem))
func displayError()
}
4. 모델
- ModelStateProtocol 및 ModelActionsProtocol을 상속하여 구현
- 뷰(SearchModelStateProtocol)에 표시할 상태(=모델) 구현
- 인텐트(SearchModelActionsProtocol)에서 데이터 또는 이벤트를 수신하고 처리하는 로직 구현
- ObservableObject + @Published 선언
- 뷰에 즉시 전달되고 변경 시 업데이트되는 변수(=모델=상태)
- 변경이 감지된 후 즉시 업데이트해야 하는 변수가 아니면 @Published 선언이 필요하지 않습니다.
final class SearchModel: ObservableObject, SearchModelStateProtocol {
@Published var contentState: ContentState = .loading
@Published var navigationTitle: String = "검색 중.."
let loadingText: String = "잠시만 기다려주세요💨"
let errorText: String = "사진 불러오기에 실패했습니다🥺"
}
extension SearchModel: SearchModelActionsProtocol {
func displayLoading() {
contentState = .loading
}
func updateImage(images: (ImageItem)) {
navigationTitle = "검색 결과🔍"
contentState = .content(images: images)
}
func displayError() {
navigationTitle = "에러 발생"
contentState = .error
}
}
5. 의도 프로토콜
- View에서 이벤트를 수신하기 위한 로그
- View에서 발생하는 이벤트에 대한 기능을 정의합니다. B.) 탭 버튼
protocol SearchIntentProtocol {
func viewOnAppear()
}
6. 의도
- View에서 이벤트가 수신되면 실행될 비즈니스 로직 구현
- 의도는 ModelActions(ModelStateProtocol X)에만 액세스합니다.
- 서버 통신이 있는 경우 인텐트에서 로직을 실행한 후 결과를 모델에 전달하여 일부 작업을 처리합니다.
import SwiftUI
class SearchIntent {
private weak var model: SearchModelActionsProtocol?
init(model: SearchModelActionsProtocol) {
self.model = model
}
}
extension SearchIntent: SearchIntentProtocol {
func viewOnAppear() {
model?.displayLoading()
ImageService.shared.fetchImageSearchData { (weak self) response in
guard let self = self else { return }
switch response {
case .success(let data):
guard let data = data as? ImageEntity else { return }
let images = data.items.map { $0.toModel(id: 0) } // entity를 UI에 적용하기 좋은 model로 변환
self.model?.updateImage(images: images)
default:
self.model?.displayError()
}
}
}
}
7.컨테이너
- 인텐트와 모델을 받는 클래스
- 의도 및 모델에 대한 참조 구현
- 초기의 modelChangePublisher
- 모델의 변경 사항 감지
- modelChange 게시자: model.objectWillChange
- 모델의 변경이 감지되면 컨테이너가 아닌 보기와 연결됩니다.
- 모델의 변경 사항 감지
import SwiftUI
import Combine
final class MVIContainer<Intent, Model>: ObservableObject {
let intent: Intent
let model: Model
private var cancellable: Set<AnyCancellable> = ()
init(intent: Intent, model: Model, modelChangePublisher: ObjectWillChangePublisher) {
self.intent = intent
self.model = model
modelChangePublisher
.receive(on: RunLoop.main)
.sink(receiveValue: objectWillChange.send)
.store(in: &cancellable)
}
}
8.보기.빌드()
- 모델과 인텐트를 묶는 컨테이너가 포함된 뷰를 생성하고 반환하는 함수
- ContentView()가 아닌 .build()를 통해 컨테이너를 포함한 뷰 반환
- @StateObject 속성으로 컨테이너 선언
- 보기를 다시 만들 때 의도와 모델이 다시 만들어지지 않도록 합니다.
import SwiftUI
// MARK: - ContentView
struct ContentView: View {
@StateObject var container: MVIContainer<SearchIntentProtocol, SearchModelStateProtocol>
...
}
// MARK: - ContentView+Build
extension ContentView {
/// Container와 View 생성
static func build() -> some View {
let model = SearchModel()
let intent = SearchIntent(model: model)
let container = MVIContainer(intent: intent as SearchIntentProtocol,
model: model as SearchModelStateProtocol,
modelChangePublisher: model.objectWillChange)
let view = ContentView(container: container)
return view
}
}
// MARK: - Pinterest_MVIApp
@main
struct Pinterest_MVIApp: App {
var body: some Scene {
WindowGroup {
ContentView.build()
}
}
}
9. 이벤트 포워딩 + 모델로 구성 보여주기
- 사용자 이벤트가 표시되면 container.intent의 인텐트로 전달됩니다.
- container.model에 대한 모델(= 상태 = 데이터) 가져오기 및 표시
import SwiftUI
struct ContentView: View {
@StateObject var container: MVIContainer<SearchIntentProtocol, SearchModelStateProtocol>
private var state: SearchModelStateProtocol { container.model }
private var intent: SearchIntentProtocol { container.intent }
var body: some View {
NavigationView {
ZStack {
switch state.contentState {
case .loading:
Text(state.loadingText)
case .content(let images):
ScrollView {
LayoutView(imageData: images)
.padding()
}
default:
Text(state.errorText)
}
}
.onAppear {
intent.viewOnAppear()
}
.navigationTitle(state.navigationTitle)
}
}
}
4⃣ MVI의 장단점
데이터가 어디에서 오고 어디로 가는지 이해하기 쉽기 때문에 코드가 더 간단합니다.
요인이 과대 평가될 가능성이 적습니다.
그리고 모델 자체가 상태가 되어 새로운 모델을 제공하면 그에 따라 새로운 뷰가 그려집니다.
이 프로세스에 따르면 State와 View는 항상 동일한 상태, 즉 업데이트된 상태입니다.
혜택
– 데이터 흐름이 한 방향으로 설정되어 흐름을 이해하고 관리하기 쉽습니다.
– 상태 문제(부작용) 발생 가능성 감소
단점
– 다른 MV에 비해 높은 학습 곡선*
– 작은 변화라도 의도가 있어야 하므로 작은 앱이라도 최소한의 의도와 모델이 있어야 합니다.
참고문헌
https://broken-bytes.medium.com/using-the-mvi-pattern-in-swift-ios-app-development-72d7881d0dc2
https://betterprogramming.pub/mvi-architecture-for-swiftui-apps-cff44428394
