Notice
Recent Posts
Recent Comments
Link
관리 메뉴

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

[iOS - swift] MVVM구조에서 UITableView 사용 시, dataSource 관리 방법 (#프로토콜 지향 프로그래밍) 본문

iOS 응용 (swift)

[iOS - swift] MVVM구조에서 UITableView 사용 시, dataSource 관리 방법 (#프로토콜 지향 프로그래밍)

jake-kim 2023. 5. 28. 01:33

dataSource 관리 방법

  • dataSource를 관리하다보면 보통 UI에서 사용되는 데이터와 비즈니스로직이 들어갈 ViewModel에서 사용되는 dataSource 두 벌을 관리하는 경우가 있는데, 상태 관리를 한곳에서 사용하지 않으면 데이터 정합성 문제가 발생
  • protocol을 사용하여 dataSource를 관리하면 해결
  • viewController에서 dataSource에 접근할 때는 protocol에 의존하게끔하면 해결

MVVM 구조 먼저 준비

  • MVVM구조를 쉽게 구현하기 위해 예제에 사용할 라이브러리
pod 'RxSwift'
pod 'RxCocoa'
  • View
    • ViewModel 구현체에 의존하지 않도록, ViewModelable 프로토콜을 생성하고 이것에 의존하도록 구현
    • UI에 관한 input을 쉽게 viewModel에 전달하기 위해서 enum으로 Action 정의
import UIKit
import RxCocoa
import RxSwift

enum Action {
    case viewDidLoad
}

protocol ViewModelable {
    var output: Observable<State> { get }
    
    func input(_ action: Action)
}

class ViewController: UIViewController {
    // MARK: Properties
    let viewModel: ViewModelable = ViewModel()
    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
        bind()
    }
    
    private func configureUI() {
        // TODO
    }
    
    private func bind() {
        
    }
}
  • ViewModel
    • UI에서 오는 인풋을 처리하는 함수를 구현하고, UI에서 해당 ViewModel로 상태에 관한 바인딩을 시킬 것이므로 enum State와 output을 정의
    • 내부적으로 outputSubject로 State를 방출하고, 이것은 output에서 구독하고 있어서 UI쪽에서 바인딩할 수 있도록 구현
import RxSwift
import RxCocoa

enum State {
    case updateUI(itmes: [String])
}

final class ViewModel: ViewModelable {
    // MARK: State
    
    // MARK: Output
    var output: RxSwift.Observable<State> {
        outputSubject
    }
    private var outputSubject = PublishSubject<State>()
    
    // MARK: Input
    func input(_ action: Action) {
        switch action {
        case .viewDidLoad:
            print("todo")
        }
    }
}
  • DataSourceable이라는 프로토콜을 생성하여 viewModel에서 dataSource를 가지도록 선언
protocol DataSourceable {
    var dataSource: [String] { get }
}

protocol ViewModelable: DataSourceable {
    var output: Observable<State> { get }
    
    func input(_ action: Action)
}
  • ViewModel에 데이터가 업데이트 되었다는 이벤트만 주면, viewController에서는 viewModel.dataSource로 접근이 가능하여 viewModel 한곳에서만 dataSource를 관리하도록 정의

ViewController) UI에서 viewDidLoad이벤트를 viewModel에 던짐

// ViewController

override func viewDidLoad() {
	...
    viewModel.input(.viewDidLoad)
}

ViewModel) UI에서 viewDidLoad이벤트가 오면 dataSource를 초기화한 후 updateUI이벤트 방출

// ViewModel

enum State {
    case updateUI
}

final class ViewModel: ViewModelable {
    // MARK: State
    var dataSource = (1...10).map(String.init)
    
    // MARK: Output
    var output: RxSwift.Observable<State> {
        outputSubject
    }
    private var outputSubject = PublishSubject<State>()
    
    // MARK: Input
    func input(_ action: Action) {
        switch action {
        case .viewDidLoad:
            print("todo")
        }
    }
}

ViewController) viewModel을 바인딩 하고 있다가 업데이트요청이 오면 reload 시키고, cellForRow에서는 viewModel의 dataSource를 바라보고 있는 상태

// ViewController

private func bind() {
    viewModel.output
        .observe(on: MainScheduler.instance)
        .bind { [weak self] state in
            guard let self else { return }
            
            switch state {
            case .updateUI:
                tableView.reloadData()
            }
        }
        .disposed(by: disposeBag)
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        viewModel.dataSource.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
        cell?.textLabel?.text = viewModel.dataSource[indexPath.row]
        return cell!
    }
}

(완성)

결과 - 데이터들이 정상 반영

* 전체 코드: https://github.com/JK0369/ExDataSourceProtocol

Comments