RxSwift 마지막의 마지막편 !!!

2020. 8. 21. 16:54iOS/RxSwift

 

 

hereismyblog.tistory.com/12

 

RxSwift 대망의 마지막 편..1

안녕하세요 !!! 오늘은 RxSwift를 마지막으로 다루어 보겠습니다. 이전 동안 유튜브 '곰튀김'님의 강의영상을 보고 쭉 진행해 왔었는데요 ! 오늘 무려 두시간 동안 강의를 쭉 보면서 실습을 해보았�

hereismyblog.tistory.com

지난 편 에 이어서 RxSwift의 마지막을 향해 달려가 보도록 하겠습니다.

Subject를 사용하여 초기 메뉴 데이터를 바꿔주기도 하고 각 메뉴 리스트 데이터를 '감시'하는 스트림을 생성하고 그 스트림에서 다른 줄기를 만들어 각 메뉴가 선택된 개수에 따라 총 아이템 개수와 총 가격이 바뀌는 것을 볼 수 있었습니다.

따라서 이제는 각 테이블 셀에서 선택한 아이템 개수를 조절하는 것을 해보겠습니다.

테이블뷰를 구현할 때 일반적으로 데이블뷰 셀을 구성하는 클래스를 생성하여 만들어 줍니다. 그렇다면 그 셀 안에서 뷰모델에 대한 동작은 어떻게 구현해야 할까요 ?

일반적으로 두가지 방법이 있다고 합니다. 버튼에 대한 동작을 클로져를 통해 셀을 뷰컨에 불러오는 시점에서 정의해 주는 것과, 셀 안에서 뷰모델을 직접 참조하여 동작하는 방법이 있다고 합니다.

저는 첫번째 방법으로 동작을 해보겠습니다.

앞서서 RxCocoa를 통해 이러한 Observable데이터를 테이블뷰에서 구현하는 방법에 대해 알아보았습니다. 따라서 각 셀 클래스를 작성할 때 메뉴 추가 또는 제거 버튼에 대한 @IBAction을 설정해 두고 클로져를 호출하도록 합니다.

class MenuItemTableViewCell: UITableViewCell {
    @IBOutlet var title: UILabel!
    @IBOutlet var count: UILabel!
    @IBOutlet var price: UILabel!

    var onChaged: ((Int) -> Void)?
    
    @IBAction func onIncreaseCount() {
       
        onChaged?(+1)
    }

    @IBAction func onDecreaseCount() {
        onChaged?(-1)
    }
}

 

따라서 테이블뷰 셀을 구성할 때 클로저를 직접 정의해 주면 됩니다.

        viewModel?.menuObservable
            .bind(to: tableView
                .rx
                .items(cellIdentifier: cellID,
                       cellType: MenuItemTableViewCell.self)) { index, item, cell in
                        // index : index
                        // item : menu 배열중 하나가 들어온다.
                        // cell : 지정해준 Cell의 인스턴스
                        cell.title.text = item.name
                        cell.price.text = "\(item.price.currencyKR())"
                        cell.count.text = "\(item.count)"
                        // +,- 버튼에 대한 클로져 
                        cell.onChaged = { [weak self] increase in
                            self?.viewModel?.changeCount(item, increase)
                        }
        }.disposed(by: self.disposeBag)

이제 뷰모델에서 메뉴데이터에 대한 이벤트를 작성해주면 됩니다. 

    func changeCount(_ menu: Menu, _ increase: Int){
        _ = menuObservable.map {
            return $0.map {
                if $0.name == menu.name {
                    return Menu(name: $0.name, price: $0.price, count: $0.count + increase)
                }
                else {
                    return Menu(name: $0.name, price: $0.price, count: $0.count)
                }
            }
        }
        .take(1) // 얘는 한번만 수행할 놈이다라는 것을 말해준다.
        .subscribe(onNext: {
            self.menuObservable.onNext($0)
        }) // Dispose를 하지 않아도 종료됨
    }

여기서 새롭게 보게되는 함수는 take라는 함수 입니다. take는 

func take(_ count: Int) -> Observable<[Menu]>

다음과 같이 구성되어 있습니다. 이 함수는 스트림의 이벤트가 들어오는 횟수를 지정하여 그 수를 넘기면 자동으로 종료된다고 합니다. 따라서 위의 메뉴 개수를 늘이고 줄이는 이벤트에 따라 매번 스트림이 계속 생성되는 것이 아니라 이런 이벤트가 발생했을 때 임시의 스트림을 만들어 변경해준 다음 이벤트를 전달하고 자동으로 스트림이 종료가 되도록 할 수 있습니다.

이와 비슷하게 Clear버튼을 통해 선택된 전체 메뉴를 지우는 것도 이와 비슷하게 볼 수 있습니다.

    // 각 동작은 뷰모델 안에서 수행
    func clearAllItemSelections() {
        _ = menuObservable.map {
            return $0.map {
                return Menu(name: $0.name, price: $0.price, count: 0)
            }
        }
        .take(1) // 얘는 한번만 수행할 놈이다라는 것을 말해준다.
        .subscribe(onNext: {
            self.menuObservable.onNext($0)
        }) // Dispose를 하지 않아도 종료됨
        
        // 클리어 버튼이 수행될때 마다 계속해서 스트림이 만들어 진다.
    }

Clear버튼을 눌렀을 때의 동작은 뷰모델의 데이터를 조작하는 것으로 시작합니다. 모든 count값을 0으로 하는 데이터를 새롭게 전송하게 되만 이벤트가 끝나면 자동으로 종료됩니다.

이제 다른 여러가지 Sugar API에 대해 알아보겠습니다.

앞서 UI 작업은 MainThread에서만 동작을 하도록 합니다. 또한 스트림에서 에러가 날 경우 그 스트림의 연결이 끊어지게 되면서 UI업데이트 동작이 되지 않게 됩니다.

이런 에러에 대응하기 위해서 알아두어야 할 메소드가 있습니다.

        viewModel?.totalPrice
            .map { $0.currencyKR() }
            .catchErrorJustReturn("")
            .observeOn(MainScheduler.instance)
            .bind(to: self.totalPrice.rx.text)
            .disposed(by: self.disposeBag)
        
func catchErrorJustReturn(_ element: String) -> Observable<String>

 

라는 메소드는 스트림에서 에러가 발생할 경우 다음과 같이 스트림이 끊어지는 것이 아닌 지정한 데이터를 넘겨줄 수 있도록 에러 핸들링이 가능합니다.

또한 이 두가지 메인스레드 호출과 에러핸들링을 한번에 할 수 있는 메소드가 존재합니다.

        viewModel?.totalPrice
            .map { $0.currencyKR() }
    	    .asDriver(onErrorJustReturn: "")
            .drive(self.totalPrice.rx.text)
            .disposed(by: self.disposeBag)

.asDriver 와 drive를 통해 에러에 대한 핸들링과 메인스레드 호출을 같이 할 수 있습니다.

또한 이런 끊어지지 않는 Subject가 있습니다. import RxRelay를 통해 사용이 가능하고 BehaviorRelay라는 것을 사용하여 이런 UI를 담당하는 subject를 생성할 수 있습니다.

이 relay에서는 에러에 대한 처리 자체가 없기 때문에 onNext가 존재하지 않고 스트림에서 무조건적으로 수용가능 하기 때문에 .accept라는 함수를 사용하는 특징이 있습니다.