Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - SwiftUI] SwiftUI+MVVM (SwiftUI에서 MVVM 사용 방법, View, ViewModel) 본문

iOS 응용 (SwiftUI)

[iOS - SwiftUI] SwiftUI+MVVM (SwiftUI에서 MVVM 사용 방법, View, ViewModel)

jake-kim 2022. 9. 27. 23:50

MVVM 

핵심은 View와 ViewModel이고 각 역할을 기억

  • View: ViewModel에서 상태가 변하면 그 상태를 단순히 구독하고 있다가 View를 변경하는 역할
  • ViewModel: 상태 값을 저장하고 있고, 상태 값을 관리(계산 등)를 하는 역할

View와 ViewModel 구현 핵심

  • View에서 특정 UI의 action이 발생하면 ViewModel에 던져줌
  • ViewModel에서는 액션에 따라 특정 상태값을 관리하고 상태값을 변경
  • ViewModel의 상태값을 바라보고 있는 View는 그에 맞추어서 UI 변경

주의사항) 상태 관리 포인트는 View가 아니라 ViewModel이므로, 상태 관련 코드는 ViewModel 한 곳에서 수행되도록 할 것

View, ViewModel 구현 아이디어

  • enum을 사용하여, viewModel의 명확한 상태를 보여주도록 구현
  • ViewModel의 공통적인 형태를 정의해주기 위해서 ViewModelable이라는 프로토콜을 정의
// ViewModelable.swift

import SwiftUI
import Combine

protocol ViewModelable: ObservableObject {
  associatedtype Action
  associatedtype State
  
  var state: State { get }
  
  func action(_ action: Action)
}
  • ViewModel에서는 enum타입으로 Action과 State를 정의하고, View에서는 해당 state를 바라보고 있다가 적절히 뷰를 변경하는 것
    • View -> ViewModel: action(_:)함수로 전달
    • ViewModel -> View: 뷰쪽에서 state를 바라보고 있다가 업데이트

예제로 구현할 뷰) Counter

ViewModel 구현

  • ViewModelable을 준수하도록 구현
import SwiftUI
import Combine

final class CounterViewModel: ViewModelable {
  // MARK: Types
  enum Action {
  }
  enum State {
  }
  
  // MARK: Properties
  @Published var state: State
  
  // MARK: Initailizer
  init() {
  }
  
  // MARK: Action
  func action(_ action: Action) {
  }
}
  • State 정의
    • count 값 (뷰에 표출될 값)
  enum State {
    case count(Int)
  }
  • action 정의
    • View에서 Add버튼과 Subtract버튼이 있으므로 두 가지 상태 정의
  enum Action {
    case onTapAddButton
    case onTapSubtractButton
  }
  • 위 action에 따라 상태를 변경해줄 action 함수 구현
  func action(_ action: Action) {
    switch action {
    case .onTapAddButton:
      state = .count(getCurrnetCount() + 1)
    case .onTapSubtractButton:
      state = .count(getCurrnetCount() - 1)
    }
  }
  
  private func getCurrnetCount() -> Int {
    guard case let .count(int) = state else { return 0 }
    return int
  }
  • 구현 완료된 CounterViewModel
import SwiftUI
import Combine

final class CounterViewModel: ViewModelable {
  // MARK: Types
  enum Action {
    case onTapAddButton
    case onTapSubtractButton
  }
  enum State {
    case count(Int)
  }
  
  // MARK: Properties
  @Published var state: State
  
  // MARK: Initailizer
  init() {
    state = .count(0)
  }
  
  // MARK: Action
  func action(_ action: Action) {
    switch action {
    case .onTapAddButton:
      state = .count(getCurrnetCount() + 1)
    case .onTapSubtractButton:
      state = .count(getCurrnetCount() - 1)
    }
  }
  
  private func getCurrnetCount() -> Int {
    guard case let .count(int) = state else { return 0 }
    return int
  }
}

View 구현

  • View는 @ObservedObject로 viewModel을 가지고 있는 형태
  • View 관련 상태는 모두 viewModel에서 관리하도록 구현 (상태 관리는 ViewModel 한 곳에서만하여 관리포인트를 한곳으로 제한)
import SwiftUI
import Combine

struct CounterView: View {
  // MARK: Properties
  @ObservedObject var viewModel: CounterViewModel
  
  // MARK: UI
  var body: some View {
  }
}
  • contentView를 따로 놓고 이곳에서 viewModel의 상태에 따라 뷰를 변경하도록 구현
import SwiftUI
import Combine

struct CounterView: View {
  // MARK: Properties
  @ObservedObject var viewModel: CounterViewModel
  
  // MARK: UI
  var body: some View {
    NavigationView {
      self.contentView
        .navigationTitle(Text("Counter 화면"))
    }
  }

  @ViewBuilder
  private var contentView: some View {
    // TODO
  }
}
  • contentView에서 viewModel의 상태에 따라 뷰 적용
  @ViewBuilder
  private var contentView: some View {
    switch viewModel.state {
    case let .count(int):
      VStack(alignment: .center, spacing: 20) {
        getCountView(count: int)
        HStack(alignment: .center, spacing: 50) {
          getSubtractButtonView()
          getAddButtonView()
        }
      }
    }
  }
  • 위에서 호출하는 getCountView(count:), getAddButtonView(), getSubtractButtonView() 메소드 구현
  @ViewBuilder
  private func getSubtractButtonView() -> some View {
    Button("-") {
      viewModel.action(.onTapSubtractButton)
    }
    .font(.largeTitle)
  }
  
  @ViewBuilder
  private func getAddButtonView() -> some View {
    Button("+") {
      viewModel.action(.onTapAddButton)
    }
    .font(.largeTitle)
  }
  
  @ViewBuilder
  private func getCountView(count: Int) -> some View {
    Text("\(count)")
      .font(.title)
  }

(완성)

* 전체 코드: https://github.com/JK0369/ExMVVM-SwiftUI

Comments