# TableView

RxCocoa에서는 델리게이트를 사용하지 않는다. 옵저버블을 뷰에 바인딩하는 방식으로 처리한다. 주로 배열데이터를 테이블뷰에 바인딩하는 형태로 처리하게 되는데, 이때 RxCocoa TableView 익스텐션의 items 메서드를 사용하게 된다.

let nameObservable = Observable.of(appleProducts.map{ $0.name })
let productObservable = Observable.of(appleProducts)

nameObservable.bind(to: listTableView.rx.items) { tableView, row, element in
    let cell = tableView.dequeueReusableCell(withIdentifier: "standardCell")!
    cell.textLabel?.text = element
    return cell
}
.disposed(by: bag)

기본 셀을 사용하는 경우 bind 연산자를 사용하면 되고, 테이블뷰의 rx.items를 파라미터로 전달한다. 클로저에서는 소스 테이블뷰, row 인덱스, 바인딩되는 데이터가 파라미터로 할당된다.

nameObservable.bind(to: listTableView.rx.items(cellIdentifier: "standardCell")){ row, element, cell in
        cell.textLabel?.text = element
    }
    .disposed(by: bag)

셀 id값을 지정하는 경우 cellIdentifier 파라미터를 전달하면 된다.

productObservable.bind(to: listTableView.rx.items(cellIdentifier: "productCell", cellType: ProductTableViewCell.self)){ [weak self] row, element, cell in
    cell.categoryLabel.text = element.category
    cell.priceLabel.text = self?.priceFormatter.string(for: element.price)
    cell.productNameLabel.text = element.name
    cell.summaryLabel.text = element.summary
}
.disposed(by: bag)

커스텀 셀을 사용하는 경우 셀 아이디값과 cellType클래스 메타타입을 전달하면 된다.

아래는 테이블뷰 아이템 셀렉트 이벤트를 처리하는 코드이다. 드라이버를 사용하여 선택된 셀의 객체 name속성을 출력한다.

listTableView.rx.itemSelected.asDriver()
    .drive{ [weak self] in
        self?.listTableView.deselectRow(at: $0, animated: true)
        print(appleProducts[$0.row].name)
    }
    .disposed(by: bag)

컨트롤이벤트의 클로저 파라미터로 IndexPath가 자동으로 전달된다.

MVVM과 modelSelected

modelSelected는 뷰에서 바인딩하는 데이터를 옵저버블 형태로 생성해둔 모델을 기준으로 타입캐스팅을 진행한다.

 productObservable.bind(to: listTableView.rx.items(cellIdentifier: "productCell", cellType: ProductTableViewCell.self)){ [weak self] row, element, cell in
            // UI 바인딩 작업
        }
        .disposed(by: bag)

모델에 직접 접근하는 방식은 올바르지 않다. 데이터 접근권한이 뷰모델에만 존재하기 때문에 뷰 내에서 배열 형태로 데이터에 접근하는 방식보다는 이벤트 방출시 나온 데이터를 직접 사용하는 것이 좋다.

위의 코드를 개선하면 다음과 같다.

listTableView.rx.modelSelected(Product.self)
            .bind{ print($0.name) }
            .disposed(by: bag)

modelSelected 연산자를 사용하며, 파라미터에 메타타입을 전달한 뒤 바인딩을 진행한다.

테이블뷰 deselect를 위한 IndexPath데이터를 가져와야 한다면 itemSelected 컨트롤 이벤트의 클로저를 사용하면 된다. 이들 각각 따로 정의해도 괜찮지만, zip 연산자를 통해 하나의 옵저버블로 묶어도 된다.

zip 연산자는 indexed 시퀀싱을 기반으로 동작한다.

Observable.zip(listTableView.rx.modelSelected(Product.self), listTableView.rx.itemSelected)
    .bind{ [weak self] product, indexPath in
        print(product.name)
        self?.listTableView.deselectRow(at: indexPath, animated: true)
    }
    .disposed(by: bag)

코코아터치 프레임워크 자체 델리게이트 프로토콜 채택 후 메서드 구현시 RxCocoa를 기반으로 구현해둔 코드들은 동작하지 않게 된다.

RxCocoa 기반에서 델리게이트를 지정하는 방법은 기존과 다르다. rx.setDelegate(뷰컨) 메서드를 사용한다. 델리게이트 설정 후 dispose까지 완료해야한다.

델리게이트 연결 방식에만 차이가 있고, 메서드 구현 방식은 이전처럼 프로토콜 채택 후 내부 구현을 진행하면 된다.

listTableView.rx.setDelegate(self)
    .disposed(by: bag)

extension RxCocoaTableViewViewController: UITableViewDelegate{
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("SELECTED")
    }
}

컬렉션뷰는 테이블뷰와 거의 유사하게 동작한다.

# AlertController

RxCocoa에서 알럿 컨트롤러는 알럿 객체 생성, 액션 등록이라는 흐름은 동일하지만 액션 등록시 파라미터에서 동작을 정의하지 않고 이벤트를 방출한다는 점에서 차이가 있다.

또한 옵저버블의 create 연산자 내에서 알럿 객체 정의가 이루어지고, 클로저 내에서 Disposables를 생성할때에 알럿을 다시 dismiss한다.

enum ActionType {
    case ok
    case cancel
}

func info(title: String, message: String? = nil) -> Observable<ActionType>{
    return Observable<ActionType>.create{ [weak self] observer in
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)

        let okAction = UIAlertAction(title: "OK", style: .default){ _ in
            observer.onNext(.ok)
            observer.onCompleted()
        }

        alert.addAction(okAction)

        self?.present(alert, animated: true)
        return Disposables.create {
            alert.dismiss(animated: true)
        }
    }
}
  1. info함수는 뷰에서 호출할 커스텀 함수이다.
  2. ActionType은 알럿뷰 액션을 분기처리하기 위한 열거형 타입이다.
  3. 알럿액션 정의시 클로저에서 onNext 이벤트를 방출한다. 이때 옵저버블의 제네릭 타입이 ActionType으로 지정되어 있기 때문에 넥스트 이벤트로 해당 열거형 인스턴스가 전달된다.
  4. 넥스트 이벤트 전달 후에 그 즉시 onCompleted 이벤트를 방출한다.
  5. 정의한 액션을 알럿 객체에 연결하고, 뷰에 present한다.
  6. 디스포저블을 리턴하면서 클로저를 종료한다. 이때 알럿창을 dismiss해줘야 한다. (옵저버블이 디스포즈되는 시점이다.)

위의 info 함수를 아래와 같이 다루게 된다.

버튼.rx.tap
    .flatMap{ [unowned self] in
        self.info(title: "컬러", message: self.colorView.backgroundColor?.rgbHexString ?? "")
    }
    .subscribe(onNext: { [weak self] actionType in
        switch actionType{
        case .ok:
            print(self?.colorView.backgroundColor?.rgbHexString ?? "")
        default:
            break
        }
    })
    .disposed(by: bag)

참고삼아 위의 코드를 flatMap이 아닌 map으로 구현한 코드를 살펴보자.

let observable = oneActionAlertButton.rx.tap
    .map { [unowned self] in
        return self.info(title: "컬러", message: self.colorView.backgroundColor?.rgbHexString ?? "")
    }
    // .. 나머지 구독 코드

tap컨트롤 이벤트는 옵저버블이다. 해당 이벤트에 대해 map 연산자를 호출하게 되면, self.info에서 리턴하는 값이 Observable<ActionType>이므로 Observable<Observable<ActionType>>이 최종 타입이 된다.

flatMap 클로저 파라미터에서 self.info 리턴 옵저버블이 클로저의 리턴 타입이 된다.

따라서 flatMap연산자의 리절트 옵저버블(Result Observable)의 타입은 Observable<ActionType>이 된다.

한 옵저버블을 다른 타입의 옵저버블로 바꿀때에도 flatMap을 유용하게 사용할 수 있겠다.

# Notification

toggleButton.rx.tap
    .subscribe(onNext: { [unowned self] in
        if self.textView.isFirstResponder {
            self.textView.resignFirstResponder()
        } else {
            self.textView.becomeFirstResponder()
        }
    })
    .disposed(by: bag)

let willShowNotification = NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
    .map{ ($0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0}

let willHideNotification = NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification )
    .map{ noti -> CGFloat in 0}

Observable.merge(willShowNotification, willHideNotification)
    .map{ [unowned self] height -> UIEdgeInsets in
        var inset = self.textView.contentInset
        inset.bottom = height
        return inset
    }
    .subscribe(onNext: { [weak self] inset in
        UIView.animate(withDuration: 0.3) {
            self?.textView.contentInset = inset
        }
    })
    .disposed(by: bag)

RxCocoa에서의 노티피케이션은 Observable<Notification>을 리턴한다. 파라미터로 노티피케이션 이름을 받는다.

노티피케이션의 이름은 UIResponder에 기본적으로 정의되어 있다. 여기서는 수치 자체를 리턴하기에 옵저버블이 중첩되지 않는다. 따라서 map연산자를 통해 Notificaton옵저버블을 다른 타입의 옵저버블로 평탄화 없이 바꿀 수 있다.

두 옵저버블을 병합하고, 토글버튼의 구독자에 대한 이벤트 방출에 따라 달라지는 노티피케이션에 대해 willShowNotification 옵저버블이 이벤트를 방출할지, willHideNotification 옵저버블이 이벤트를 방출할지 결정된다.

merge연산자는 병합된 두 옵저버블 모두 onCompleted를 방출할때까지 살아있다.

# GestureRecognizer

RxCocoa에서 제스처는 새로운 이벤트가 발생할때마다 next 이벤트를 전달하게끔 내부적으로 구현되어 있다.

생성해둔 GestureRecognizer 객체의 rx.event속성에 접근하여 코드를 작성한다.

panGesture.rx.event
    .subscribe(onNext: { [weak self] in
        let translation = $0.translation(in: self?.view)
        $0.view?.center.x += translation.x
        $0.view?.center.y += translation.y

        $0.setTranslation(.zero, in: self?.view)
    } )
    .disposed(by: bag)

구독자에게 전달되는 next이벤트의 클로저 파라미터는 옵저버이며, 해당 타입은 ControlEvent<UIPanGestureRecognizer>.Element이다.