본문 바로가기

iOS

[RxSwift] RxSwift 기본 (2편 - RxCocoa)

[Swift] RxSwift 기본 1편 : Observable, Observer, Subject

RXCocoa

RxCocoa는 UI 작업에 필수적인 라이브러리입니다.

RxCocoaRxSwift의 companion 라이브러리로, UIKit과 Cocoa 프레임워크 기반 모든 클래스를 가지고 있습니다! (RxSwift는 UIKit에 대한 정보가 없음)

RxCocoa를 사용하여 UIButton의 Tap 이벤트, 테이블뷰의 delegate Method(didSelectItemAt)등 여러가지 기능들을 쉽게 사용할 수 있습니다~!

아래는 RxCocoa에서 UIKit의 여러 요소들을 익스텐션하는 모습입니다.

📌 Relay

RxCocoa는 UI 작업을 돕기 위한 라이브러리인 만큼, Subject를 Wrapping하여 UI 작업에 용이하게끔 만든 Relay라는 클래스를 제공합니다!

RxSwift의 Observable, Subject와는 다르게 Relay는 Complete, Error 이벤트를 받지 않는다는 것이 가장 큰 특징이기 때문에, 개발자의 의도적인 dispose() 없이는 영원히 유지될 수 있다는 특징을 가집니다! 위의 계속하여 유지될 수 있다는 점이 Relay라는 이름의 이유이기도 할거라는 추측입니다...

 

예제를 보시죠!

let relay = BehaviorRelay<String>(value: "안뇽")
let disposeBag = DisposeBag()

relay
  .subscribe(onNext: { print($0) })
  .disposed(by: disposeBag)
relay.accept("3")
print("현재 값에 접근: ", relay.value)

/* prints
안뇽
3
현재 값에 접근: 3
*/

subject와 다른 점은, relay는 onNext() 로 이벤트를 받지 않고 accept()로 받는다는 것입니다.

이러한 네이밍의 이유는, relay가 절대로 종료될 일이 없기 때문에(error나 complete가 없으니)! 값은 **항상 받기만(accept)**하기 때문이죠!

📌 subscribe(), bind(), drive()

UI 작업에 많이 사용되는 메서드로 bind()와 drive() 메서드가 있습니다. 이들은 UI 작업의 특징을 고려하여 RxSwift의 사용에 편의성을 추가해주는데요!

둘 모두 **subscribe()**를 하는 동시에, UI 작업에 유리한 몇 가지 특징을 부여하고 있습니다.

✳️ bind()

bind는 묶다는 뜻이죠? 말 그대로 bind()는 Observable과 Observer를 하나로 묶어주는 역할이라고 생각하면 됩니다!

특징 1 : bind는 Observable이 방출한 이벤트를 그대로 binder에게 전달합니다. 그리고 binderUI 업데이트에 사용되는 Observer의 일종이라고 할 수 있어요!

특징 2 : 또한 bind는 onNext 이벤트에 대해서만 반응하고, error 이벤트가 들어오면 에러 로그를 콘솔에 출력해줍니다!

⁉️ 정의 - 옵저버 타입을 받아서, subscribe()와 동일하게 Disposable을 Return하는 것을 확인 가능합니다!

func bind<Observer>(to observers: Observer...) -> Disposable 
		where Observer : ObserverType, Observer.Element == String?

텍스트필드의 입력을 라벨의 텍스트에 즉시 반영하기 : subscribe() 사용

textField.rx.text
    .subscribe(onNext: {
  	  self.testLabel.text = $0
    })
    .disposed(by: disposeBag)

텍스트필드의 입력을 라벨의 텍스트에 즉시 반영하기 : bind() 사용

textField.rx.text.orEmpty
			.bind(to: testLabel.rx.text)
      .disposed(by: disposeBag)

어떤가요? 굉장히 간결하죠? RxSwift 없이는 editingChanged, addTarget 등등을 이용해서 구현했었는데 RxSwift를 사용하니 코드 세 줄로 해결이 되었습니다!

⁉️ 여기서 궁금증이 생기실텐데요! textField.rx.text가 무슨 코드일까요?

일단은 subscribe()할 수 있으니 Observable로 만들어주는 코드라고 유추할 수 있습니다. UI 프로퍼티에 접근해서, .rx.text 와 같이 접근해주면 Control Property라는 녀석이 되는데요! 이 친구는 Observable 타입임과 동시에 UI 작업에 편리하도록 Wrapping해준 친구입니다!

아래는 라이브러리에 있는 정의입니다!

var rx: Reactive<UITextField> { get set }
var text: ControlProperty<String?>

✳️ drive()

viewModel.output.isLoading 
	 .asDriver()
	 .drive(onNext: { [weak self] in
	 self?.indicatorView.isHidden = !$0 
	 })
	 .disposed(by: disposeBag)

/* 위와 아래는 같은 동작 코드 */ 

viewModel.output.isLoading
	 .observeOn(MainScheduler.instance)
	 .subscribe(onNext: { [weak self] in
	 self?.indicatorView.isHidden = !$0
	 })
	 .disposed(by: disposeBag)

출처: <https://nsios.tistory.com/66> [NamS의 iOS일기]

drive는 bind처럼 onNext 이벤트에 대해서만 처리합니다. 그러나 조금 다른 점은 error 이벤트를 아예 가정하고 있지 않다는 것입니다!

drive()를 사용하기 위해서는 먼저 asDriver() 메서드로 Observable에 drive trait를 부여해줘야 하는데요!

asDriver의 설명을 읽어보니... ControlProperty already can’t fail, so no special case needs to be handled. 애초에 컨트롤 프로퍼티는 실패할 수가 없기 때문에, 에러를 가정하고 있지 않다고 합니다.

 

여기에서 아까 배운 Relay가 등장합니다. Relay는 Complete, Error 이벤트를 받지 않는다라는 설명을 아까 했었죠?

그렇기에 저희는 drive를 사용할 때에, subject보다는 subject의 wrapper class인 Relay를 사용할 때 쉽게 전환할 수 있습니다.

 

또 다른 특징! drive는 메인스레드에서만 실행됩니다. UI 업데이트는 메인스레드에서만 실행되어야 하기 때문에 subscribe와 bind를 사용하는 것보다 더 안전하겠네요~~! 예를 들어 비동기 처리의 결과로 데이터를 받았을 때, drive를 사용하면 자동으로 UI 작업을 메인스레드에서 해주겠죠?

 

마지막 특징...! bind로 구독을 하면 Observer가 늘어날 때마다 Observable 스트림이 여러개가 되는데, drive를 사용하면 스트림은 하나로 유지하고 그 하나를 여러 Observer가 구독하게 되는 것입니다! 메모리 관리면에서 더 효율적이겠죠?!

textField.rx.text
        .asDriver()
        .drive(testLabel.rx.text)
        .disposed(by: disposeBag)

📌 RxCocoa를 이용한 컨트롤 이벤트 감지와 처리

아래는 뷰컨트롤러에서 일어나는 이벤트들을 어떻게 감지하고 Input으로 전달하는지에 대한 예시들입니다!

참고로 Rx Community의 RxGesture를 사용하면 Gesture의 Reactive Wrapper를 사용할 수 있습니다. https://github.com/RxSwiftCommunity/RxGesture

//buttonTapped
button.rx.tap.asObservable()

//textFieldEditingChanged
nicknameTextField.rx.text.orEmpty.asObservable()

//textFieldEndEditing
heightTextField.rx.controlEvent(.editingDidBegin).asObservable()

//pickerViewItemSelected
pickerView.rx.itemSelected.map { $0.row }

//tapGesture 
imageButton.rx.tapGesture()
    .when(.recognized)
    .map({_ in })
    .asObservable()

//viewDidLoad
Observable.just(())

//viewWillAppear
private let viewWillAppearEvent = PublishRelay<Void>()
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    self.viewWillAppearEvent.accept(())
}
self.viewWillAppearEvent.asObservable(),

//viewWillAppear
self.rx.methodInvoked(#selector(UIViewController.viewWillAppear))
    .map { _ in }

//viewDidAppear
self.rx.methodInvoked(#selector(viewDidAppear(_:)))
    .map({ _ in })

//panGesture
self.mapView.rx.panGesture()
    .when(.recognized)
    .map({ _ in })
    .asObservable()

//longPressGesture
self.cancelButton.rx
    .longPressGesture()
    .when(.began)
    .map({ _ in })
    .asObservable(),

//longPressGesture
self.cancelButton.rx
    .longPressGesture()
    .when(.ended, .cancelled, .failed)
    .map({ _ in })
    .asObservable()

//refresh
self.refreshControl.rx.controlEvent(.valueChanged).asObservable()