[ios] Data Binding in MVVM

Oxong·2022년 2월 7일
0

iOS CS Study

목록 보기
18/18

공부한 것을 정리하는 용도의 글이므로 100% 정확하지 않을 수 있습니다.
참고용으로만 봐주시고, 내용이 부족하다고 느끼신다면 다른 글도 보시는 것이 좋습니다.
+ 틀린 부분, 수정해야 할 부분은 언제든지 피드백 주세요. 😊
                                            by. Oxong




오늘 글은 Data Binding in MVVM on iOS 글을 기반으로 작성되었습니다.


ios 앱 개발, Xcode를 사용해본 사람이라면 View Controller와 View를 분리하기 힘들다는 것을 알 것이다.

View Controller가 View의 LifeCycle과 깊게 관련되어있기 때문이다.

MVVM의 기본 규칙은 아래와 같다.

1. View는 View Model을 가지고, View Model은 Model을 가진다.

2. View Model은 입출력을 처리하고, UI를 구동하는데 필요한(요구하는) 로직을 처리하는 역할만 가진다.

3. View Model은 UI를 수정할 수 없다.


따라서 View Model과 View Controller는 서로에게 데이터의 변경을 알려줄 수 있는 방법이 필요하다.

그 방법이 바로 Data Binding이다.


Data Binding은 단순히 앱의 UI (View Controller)와 UI가 표시하는 데이터 (Model 자체가 아니라 View Model의 데이터) 사이를 연결을 설정하는 프로세스이다.

Data Binding을 하는 다양한 방법이 있다. 지금부터 4가지 기법을 알아보려 한다.
(Data Binding은 MVVM에만 적용되는 것이 아니라 다른 패턴에도 적용된다!)



1. Observable


bond와 같은 바인딩 라이브러리를 제외하면, 가장 널리 사용되는 방법이다.

Observable이라는 자체 Helper Class를 만들면 이 클래스는 우리가 observe하길 원하는 값으로 초기화되고, 우리에게 binding 역할과 값을 얻어오는 역할을 하는 bind라는 함수를 제공한다.

또한, listener는 값이 변할 때마다 호출되는 클로저이다.

<<예시 코드>> 
class Observable<T> {

    var value: T {
        didSet {
            listener?(value)
        }
    }

    private var listener: ((T) -> Void)?

    init(_ value: T) {
        self.value = value
    }

    func bind(_ closure: @escaping (T) -> Void) {
        closure(value)
        listener = closure
    }
}

ViewModel 을 살펴보자.

아래 코드는 ViewModel이 채택할 프로토콜을 먼저 구현한 다음 APIManager 클래스에서 데이터(Employee)를 가져오는 코드이다.

<<예시 코드>>
import Foundation
import Alamofire

protocol ObservableViewModelProtocol {
    func fetchEmployees()
    func setError(_ message: String)
    var employees: Observable<[Employee]> { get  set } //1
    var errorMessage: Observable<String?> { get set }
    var error: Observable<Bool> { get set }
}


class ObservableViewModel: ObservableViewModelProtocol {
    var errorMessage: Observable<String?> = Observable(nil)
    var error: Observable<Bool> = Observable(false)

    var apiManager: APIManager?
    var employees: Observable<[Employee]> = Observable([]) //2
    init(manager: APIManager = APIManager()) {
        self.apiManager = manager
    }

    func setAPIManager(manager: APIManager) {
        self.apiManager = manager
    }

    func fetchEmployees() {
        self.apiManager!.getEmployees { (result: DataResponse<EmployeesResponse, AFError>) in
            switch result.result {
            case .success(let response):
                if response.status == "success" {
                    self.employees = Observable(response.data) //3
                    return
                }
                self.setError(BaseNetworkManager().getErrorMessage(response: result))
            case .failure:
                self.setError(BaseNetworkManager().getErrorMessage(response: result))
            }
        }
    }

    func setError(_ message: String) {
        self.errorMessage = Observable(message)
        self.error = Observable(true)
    }
}

1은 프로토콜의 Employees 배열에서 Observable을 선언하는 방법이다.

2는 ViewModel에서 어떻게 1을 구현하는지 보여준다.

3은 Observable에 데이터를 설정하거나 추가한다.


이제 우리는 View Controller의 viewDidLoad에서 bind를 수행할 수 있다.

<<Binding to array in viewDidLoad>>

  viewModel.employees.bind { (_) in
    self.showTableView()
  }

이제 employees의 데이터 값이 변경될 때마다 View Controller는 self.showTableView( )를 호출하게 됩니다.



2. Event Bus / Notification Center


Event Bus는 안드로이드에서 더 많이 사용된다. iOS에는 NotificationCenter로 잘 구성되어 있기 때문이다.

먼저, 모든 subscriber에게 EventBus로 보낼 이벤트를 생성한다.

(이 이벤트는 일반적으로 우리가 전달하고자 하는 내용을 포함하고 있다.)

따라서 EmployeesEvent는 employees와 Boolean 타입인 error 값, 그리고 String 타입인 errorMessage 값을 가진다.

<<예시 코드>>
import Foundation

class EmployeesEvent: NSObject {
    var error: Bool
    var errorMessage: String?
    var employees: [Employee]?

    init(error: Bool, errorMessage: String? = nil, employees: [Employee]? = nil) {
        self.error = error
        self.errorMessage = errorMessage
        self.employees = employees
    }
}

EventBus를 사용하여 ViewModel에서 이벤트를 발생시킨다.

<<예시 코드>>
func callEvent() {
    //Post Event (Publish Event)
    EventBus.post("fetchEmployees", sender: EmployeesEvent(error: error, errorMessage: errorMessage, employees: employees))
}

View Controller에서 이벤트를 구독한다.

그러면 setupEventBusSubscriber는 viewDidLoad에서 호출된다.

<<예시 코드>>
func setupEventBusSubscriber() {
    _ = EventBus.onMainThread(self, name: "fetchEmployees") { result in
        if let event = result!.object as? EmployeesEvent {
            if event.employees != nil {
                self.showTableView()
            } else if let message = event.errorMessage {
                self.showAlert(title: "Error", message: message)
            }
        }
    }
}

이제부터 EventBus의 onMainThread 구현체는 ViewModel에 있는 callEvent가 호출될 때마다 실행된다.



3. FRP Technique (RxCocoa / RxSwift)


Functional / Reactive Programming 방식을 말한다. 이는 RxCocoa나 RxSwift를 사용해서 구현할 수 있다.

두 방식 모두 RayWenderlich가 분석해놓은 자료를 참고하면 더 면밀하게 공부할 수 있다.

오늘은 그 두 가지 중에서 RxSwift를 사용해보겠다.

ViewModel 을 살펴보자.

위의 1. Observable 에서 확인한 것과 비슷하다고 느껴질 수 있다.

<<예시 코드>>
import Foundation
import Alamofire
import RxSwift

class RxSwiftViewModel {

    private let disposeBag = DisposeBag()
    private let _employees = BehaviorRelay<[Employee]>(value: [])
    private let _error = BehaviorRelay<Bool>(value: false)
    private let _errorMessage = BehaviorRelay<String?>(value: nil)

    var employees: Driver<[Employee]> {
       return _employees.asDriver()
    }

    var hasError: Bool {
       return _error.value
    }

    var errorMessage: Driver<String?> {
       return _errorMessage.asDriver()
    }

    var numberOfEmployees: Int {
       return _employees.value.count
    }

    var apiManager: APIManager?

    init(manager: APIManager = APIManager()) {
        self.apiManager = manager
    }

    func setAPIManager(manager: APIManager) {
        self.apiManager = manager
    }

    func fetchEmployees() {
        self.apiManager!.getEmployees { (result: DataResponse<EmployeesResponse, AFError>) in
            switch result.result {
            case .success(let response):
                if response.status == "success" {
                    self._error.accept(false)
                    self._errorMessage.accept(nil)
                    self._employees.accept(response.data)
                    return
                }
                self.setError(BaseNetworkManager().getErrorMessage(response: result))
            case .failure:
                self.setError(BaseNetworkManager().getErrorMessage(response: result))
            }
        }
    }

    func setError(_ message: String) {
        self._error.accept(true)
        self._errorMessage.accept(message)
    }

    func modelForIndex(at index: Int) -> Employee? {
        guard index < _employees.value.count else {
            return nil
        }
        return _employees.value[index]
    }
}

error 변수와 errorMessage 변수와 같은 employees 속성은 각각의 개인 속성에서 Driver를(Views의 컨트롤이 바인딩되는 것을 관측 가능)를 반환한다.

View Controller

<<예시 코드>>
import RxSwift

class RxSwiftController: UIViewController {

	@IBOutlet weak var tableView: UITableView!
	@IBOutlet weak var emptyView: UIView!
	@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    	let disposeBag = DisposeBag()

    	lazy var viewModel: RxSwiftViewModel = {
        	let viewModel = RxSwiftViewModel()
       		return viewModel
    	}()

	override func viewDidLoad() {
		super.viewDidLoad()
		showLoader()
		setupTableView()
        	setupBindings()
	}

    	func setupBindings() {
        	viewModel.employees.drive(onNext: {[unowned self] (_) in
            		self.showTableView()
        	}).disposed(by: disposeBag)

        	viewModel.errorMessage.drive(onNext: { (_message) in
            		if let message = _message {
                	self.showAlert(title: "Error", message: message)
            	}
        	}).disposed(by: disposeBag)
    	}

    //... other delegate methods go here
}

View Controller에서 RxSwift객체인 DisposeBag를 통해서 Observable들에 대한 참조를 해제한다.

그리고 setupBindings 메서드는 View Model의 employees 속성을 observe하게 한다.

그러면 이제부터 employees 가 변경될 때마다 showTableView 메서드가 호출되어 리스트를 reload 한다.



4. Combine


Combine 프레임워크는 Swift5.1 부터 생긴 기능으로 비동기 시그널을 캐치하고 처리하기 위해 통합된 발행 및 구독(publish-and-subscribe) API를 제공한다.

ViewModel 을 살펴보자.

먼저, View Model에 publisher를 추가해야 한다.

ViewModel에 Combine을 import하고 ObservableObject를 상속시킨다.
그 후, 우리가 observe할 emplyees 배열은 @Published로 감싸준다.
그럼 그 publisher(published로 감싸진 프로퍼티)는 프로퍼티가 변경될 때마다 현재 값(변경된 값)을 방출한다.

<<예시 코드>>
import Foundation
import Alamofire
import Combine

class CombineViewModel: ObservableObject {

    var apiManager: APIManager?
    @Published var employees: [Employee] = [] //1
    init(manager: APIManager = APIManager()) {
        self.apiManager = manager
    }

    func setAPIManager(manager: APIManager) {
        self.apiManager = manager
    }

    func fetchEmployees() {
        self.apiManager!.getEmployees { (result: DataResponse<EmployeesResponse, AFError>) in
            switch result.result {
            case .success(let response):
                if response.status == "success" {
                    self.employees = response.data
                }
            case .failure:
                print("Failure")
            }
        }
    }
}

View Controller

view controller에서 subscriberf를 publisher에 연결한다.

bindViewModel()에서 Combine의 구독자 키워드 중 하나인 sink를 사용하여 $employees를 구독한다.

이를 통해, published property이 변경될 때만 view를 업데이트 된다.

<<예시 코드>>
import UIKit
import Combine

class CombineController: UIViewController {

	@IBOutlet weak var tableView: UITableView!
	@IBOutlet weak var emptyView: UIView!
	@IBOutlet weak var activityIndicator: UIActivityIndicatorView!

    	lazy var viewModel: CombineViewModel = {
        	let viewModel = CombineViewModel()
        	return viewModel
    	}()

    	private var cancellables: Set<AnyCancellable> = []

	override func viewDidLoad() {
		super.viewDidLoad()
		showLoader()
		setupTableView()
        	bindViewModel()
	}

    	private func bindViewModel() {
        	viewModel.$employees.sink { [weak self] _ in
            		self?.showTableView()
        	}.store(in: &cancellables)
    	}
    
  	//... Other delegate methods
  
}

또한, subscriber를 인스턴스 프로퍼티에 저장하여 유지 및 자동해제가 되도록 하거나 자체적으로 취소되도록 할 수 있다.







Reference

Data Binding in MVVM on iOS

0개의 댓글