Data Binding

Groot·2022년 10월 10일
0

TIL

목록 보기
71/153
post-thumbnail

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 } //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)
    }

}

/********* Binding to array in viewDidLoad */
  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() {
    //Post Event (Publish Event)
    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)
    	}

    //... other delegate methods go here
}
  • 컨트롤러에는 관찰 중인 모든 관찰 가능 항목에 대한 참조를 해제하는 데 도움이 되는 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] = [] //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 컨트롤러에서)에 연결.
  • 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)
    	}
    
  	//... Other delegate methods
  
}
  • 그리고 구독자를 인스턴스 속성에 저장하여 유지하거나(최소한 주변 인스턴스가 사라질 때 자동으로 해제되도록) 자체적으로 취소.

📍 MVVM에서 Data Binding이 필요한 기본 원칙

  • View Model은 View가 소유하고 Model은 View Model이 소유.
  • View Model은 UI 구동에 필요한 출력과 로직에 대한 처리만을 담당.
  • View Model은 UI를 수정해서는 안된다.

Data Binding in MVVM on iOS

profile
I Am Groot

0개의 댓글