유튜브 곰튀김님의 영상을 보고 정리.
https://www.youtube.com/watch?v=iHKBNYMWd5I&list=PL03rJBlpwTaBrhux_C8RmtWDI_kZSLvdQ&index=2
Subject
Subject: 옵저버블처럼 구독할 수 있고, 옵저버블 밖에서 값을 받아 데이터를 컨트롤해서, 새로운 값을 집어넣어 줄 수 있는것.
PublishSubject
구독이 시작되고 전달되는 이벤트를 전달한다.
BehaviorSubject
기본값을 가지고 시작한다. 구독을 시작한 시점에 데이터가 전달이 안되었다면 기본값을 내려주고, 새로운 구독자가 생기면 가장 최근에 전달된 값을 전달한다.
AsyncSubject
데이터가 전달되고, 여러 구독자가 생기더라도 데이터를 내려보내주지 않는다. Complete되는 시점에 가장 마지막에 전달된 데이터를 전달하고 종료한다.
ReplaySubject
구독이 시작되고 전달된 데이터를 전달한다. 새로운 구독자가 추가되면 현재까지 전달된 데이터들을 모두 전달한다. 그 이후에 발생하는 데이터는 Publish와 같이 동일하게 전달한다.
가장 많이 사용되는건 Publish와 Behavior이다.
옵저버블이 메모리에서 정리되는건 Completed, Error이벤트가 발생했을 시기이다.
그러므로 take(1)처럼 이벤트를 한 번 받고 dispose되는 옵저버가 있다면 순환참조는 일어나지만 메모리 릭은 발생하지 않는다.
MVVM패턴
ViewModel
- 어떤 데이터를 보여줘야하느냐, 데이터에 대해 어떤 처리를 해줘야하는지는 뷰모델에 알고있다.
- 데이터의 모든 변경은 뷰모델이 처리한다. count를 감소시킨다던지, 초기화를 한다던지.. 이런 부분에 대한 처리는 ViewModel이 담당한다.
- 화면에 어떤 내용을 보여줘야하는지는 모두 ViewModel이 가지고 있다. View를 위한 Model이기 때문에 ViewModel이다.
- 모든 생각과 데이터에 대한 처리는 ViewModel이 하고, View는 데이터에대해 아는것이 아무것도 없어야 한다.
- 만약 버그가 발생해 테스트케이스를 만든다면 ViewController를 테스트하는게 쉬을까, ViewModel을 만드는게 쉬울까. ViewController를 상속받은 ViewController가 아닌 ViewModel을 만드는게 쉬울것이다.
- Controller에는 데이터를 처리하는 로직이 전혀 없기 때문에 데이터의 오류로 인해 버그가 발생할 일이 없다.
class MenuListViewModel{
// Subject 옵저버블처럼 값을 방출도하지만 옵저버처럼 값을 받을 수도 있다.
var menuObservable = BehaviorSubject<[Menu]>(value: [])
let disposeBag = DisposeBag()
lazy var itemsCount = menuObservable.map{
$0.map{ $0.count }.reduce(0, +)
}
lazy var totalPrice = menuObservable.map{
$0.map{ $0.price * $0.count }.reduce(0, +)
}
init(){
let menus: [Menu] = [
Menu(id: 1, name: "튀김1", price: 100, count: 0),
Menu(id: 2, name: "튀김1", price: 100, count: 0),
Menu(id: 3, name: "튀김1", price: 100, count: 0),
Menu(id: 4, name: "튀김1", price: 100, count: 0)
]
menuObservable.onNext(menus)
}
func clearAllItemSelections(){
menuObservable
.map{ menus in
return menus.map{
Menu(id: $0.id, name: $0.name, price: $0.price, count: 0)
}
}
.take(1)
.subscribe(onNext: {
self.menuObservable.onNext($0)
})
.disposed(by: disposeBag)
}
func changeCount(_ item: Menu, _ increse: Int){
menuObservable
.map{ menus in
return menus.map{
if $0.id == item.id{
return Menu(id: $0.id, name: $0.name, price: $0.price, count: max($0.count + increse, 0))
} else {
return Menu(id: $0.id, name: $0.name, price: $0.price, count: $0.count)
}
}
}
.take(1)
.subscribe(onNext: {
self.menuObservable.onNext($0)
})
.disposed(by: disposeBag)
}
func onOrder(){
}
}
Controller
- Controller에는 뷰의 요소만 담게된다.
- View에 어떻게 보여지게 될지, 이 요소들을 잡아서 어떤 형태로 화면에 뿌릴지만 지정한다. 어떤 로직을 담아야 하는지는 ViewModel이 가지고있다.
- 모든 데이터에 대한 처리는 ViewModel이 하게된다. 그러므로 Controller는 Model을 알 수 없다.
class MenuViewController: UIViewController {
let viewModel = MenuListViewModel()
let disposeBag = DisposeBag()
// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
viewModel.menuObservable
.bind(to: tableView.rx.items(cellIdentifier: cellID, cellType: MenuItemTableViewCell.self)){ index, item, cell in
cell.title.text = item.name
cell.price.text = "\(item.count)"
cell.count.text = "\(item.count)"
cell.onChange = { [weak self] increse in
self?.viewModel.changeCount(item, increse)
}
}
.disposed(by: disposeBag)
viewModel.itemsCount
.map{ "\($0)"}
.observeOn(MainScheduler.instance)
.bind(to: itemCountLabel.rx.text)
.disposed(by: disposeBag)
viewModel.totalPrice
.map{ $0.currencyKR() }
.observeOn(MainScheduler.instance)
.bind(to: totalPrice.rx.text)
.disposed(by: disposeBag)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let identifier = segue.identifier ?? ""
if identifier == "OrderViewController",
let orderVC = segue.destination as? OrderViewController {
// TODO: pass selected menus
}
}
func showAlert(_ title: String, _ message: String) {
let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertVC.addAction(UIAlertAction(title: "OK", style: .default))
present(alertVC, animated: true, completion: nil)
}
// MARK: - InterfaceBuilder Links
@IBOutlet var activityIndicator: UIActivityIndicatorView!
@IBOutlet var tableView: UITableView!
@IBOutlet var itemCountLabel: UILabel!
@IBOutlet var totalPrice: UILabel!
@IBAction func onClear() {
viewModel.clearAllItemSelections()
}
@IBAction func onOrder(_ sender: UIButton) {
// TODO: no selection
// showAlert("Order Fail", "No Orders")
// performSegue(withIdentifier: "OrderViewController", sender: nil)
viewModel.onOrder()
}
}