TIL
🌱 난 오늘 무엇을 공부했을까?
📌 Data Binding 알아보기
- 데이터 바인딩은 단순히 앱 UI( View Controller )과 앱이 표시하는 데이터(Not Model , But View Model ) 간의 연결을 설정하는 프로세스
📍 Technique
🔗 Observables
- Observables은 가장 쉽고 가장 일반적으로 사용되는 것
- 우리가 관찰(또는 전달)하려는 값으로 초기화되며 바인딩을 수행하고 값을 가져오는 bind 함수
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
}
}
import Foundation
import Alamofire
protocol ObservableViewModelProtocol {
func fetchEmployees()
func setError(_ message: String)
var employees: Observable<[Employee]> { get set }
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([])
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)
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)
}
}
viewModel.employees.bind { (_) in
self.showTableView()
}
🔗 Event Bus / Notification Center
- EventBuses는 Android에서 더 많이 사용된다.
- iOS에서는 NotificationCenter
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가 모든 구독자에게 푸시할 이벤트를 생성
func callEvent() {
EventBus.post("fetchEmployees", sender: EmployeesEvent(error: error, errorMessage: errorMessage, employees: employees))
}
- EventBus를 사용하여 ViewModel에서 이벤트를 게시(또는 게시)
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)
}
}
}
}
- View Controller에서 이벤트를 구독.
- 따라서 setupEventBusSubscriber는 viewDidLoad에서 호출.
- EventBus의 onMainThread 구현 내용은 ViewModel에서 callEvent가 호출될 때마다 실행.
🔗 FRP Technique (ReactiveCocoa / RxSwift)
- 함수형/반응형 프로그래밍 접근 방식으로 ReactiveCocoa 또는 RxSwift를 사용할 수 있다.
- RxSwift을 사용해 Observable 기술과 유사하게 만든 ViewModel은 다음과 같다.
import Foundation
import Alamofire
import RxSwift
import RxCocoa
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]
}
}
import RxSwift
import RxCocoa
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)
}
}
- 컨트롤러에는 관찰 중인 모든 관찰 가능 항목에 대한 참조를 해제하는 데 도움이 되는 RxSwift 객체인 DisposeBag가 있다.
- setupBindings 메서드는 뷰 모델에서 직원 속성을 관찰하는 것.
- 직원이 업데이트될 때마다 showTableView 메서드가 호출되어 목록을 다시 로드.
🔗 Combine
- Combine 프레임워크(Swift 5.1에 추가됨)는 비동기 신호를 채널링하고 처리하기 위한 통합 발행 및 구독 API를 제공.
- (ViewModel에서) 게시자를 생성.
- 이를 위해 Combine을 가져오고 ViewModel이 Combine의 ObservableOject에서 상속.
- 관찰하려는 직원 배열은 @Published 속성 래퍼로 래핑.
- 이 게시자는 속성(직원)이 변경될 때마다 현재 값을 내보낸다.
import Foundation
import Alamofire
import Combine
class CombineViewModel: ObservableObject {
var apiManager: APIManager?
@Published var employees: [Employee] = []
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 컨트롤러에서)에 연결.
- bindViewModel에서 우리는 Combine의 기본 구독자 키워드인 sink 중 하나를 사용하여 $employees를 구독하고 특정 게시된 속성이 변경될 때만 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)
}
}
- 그리고 구독자를 인스턴스 속성에 저장하여 유지하거나(최소한 주변 인스턴스가 사라질 때 자동으로 해제되도록) 자체적으로 취소.
📍 MVVM에서 Data Binding이 필요한 기본 원칙
- View Model은 View가 소유하고 Model은 View Model이 소유.
- View Model은 UI 구동에 필요한 출력과 로직에 대한 처리만을 담당.
- View Model은 UI를 수정해서는 안된다.
Data Binding in MVVM on iOS