RxCocoa가 제공하는 Traits중에서 가장 핵심적인것은 Driver이다.
Driver는UI를 Bind하는 직관적이고 효율적인 방법을 제공한다.
Driver는 특별한 옵저버블이고 UI처리에 특화된 몇 가지 특징을 가지고 있다.
1. 에러메세지를 전달하지 않는다.
오류로 인해 UI처리가 중단되는 상황은 발생하지 않는다.
2. 스케쥴러를 강제로 변경하는 경우를 제외하고 항상 메인 스케쥴러에서 작업을 수행한다.
이벤트는 항상 메인스케쥴러에서 전달되고 이어지는 작업 역시 메인스케쥴러에서 실행된다.
3. 드라이버는 사이드이펙트를 공유한다.
일번옵저버블에서 쉐어연산자를 호출하고 .whileConnect연산자를 호출한것과 동일하다.
모든구독자가 시퀀스를 공유하고 새로운 구독이 시작되면 가장 최근에 전달된 이벤트가 즉시 전달된다.
import UIKit
import RxSwift
import RxCocoa
enum ValidationError: Error {
case notANumber
}
class DriverViewController: UIViewController {
let bag = DisposeBag()
@IBOutlet weak var inputField: UITextField!
@IBOutlet weak var resultLabel: UILabel!
@IBOutlet weak var sendButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
//1. 텍스트속성에서 flatMapLatest를 호출하고 클로저에서 validateText를 리턴한다
let result = inputField.rx.text
.flatMapLatest { validateText($0) }
//3. 옵저버블이 전달하는 Bool을 ok 또는 Error로 바꾸고 레이블에 바인딩
result
.map { $0 ? "Ok" : "Error" }
.bind(to: resultLabel.rx.text)
.disposed(by: bag)
//Bool값을 빨, 파로 바구고 레이블 백그라운드와 바인드
result
.map { $0 ? UIColor.blue : UIColor.red }
.bind(to: resultLabel.rx.backgroundColor)
.disposed(by: bag)
//Bool을 버튼의 활성화상태와 바인드
result
.bind(to: sendButton.rx.isEnabled)
.disposed(by: bag)
}
}
//2. 이 함수는 String?을 파라미터로 받아서 새로운 옵저버블을 리턴한다. 옵저버블이 NE를 통해 방출하는건 Bool
func validateText(_ value: String?) -> Observable<Bool> {
return Observable<Bool>.create { observer in
//시퀀스 실행시점을 파악하기위한 로그
print("== \(value ?? "") Sequence Start ==")
defer {
print("== \(value ?? "") Sequence End ==")
}
//문자열을 Double로 컨버전하고 실패한경우 에러이벤트를 전달
guard let str = value, let _ = Double(str) else {
observer.onError(ValidationError.notANumber)
return Disposables.create()
}
//성공했다면 Next이멘트로 true를 전달하고
observer.onNext(true)
//completed를 전달
observer.onCompleted()
return Disposables.create()
}
}
/*
== 1234 Sequence Start ==
== 1234 Sequence End ==
== 1234 Sequence Start ==
== 1234 Sequence End ==
== 1234 Sequence Start ==
== 1234 Sequence End ==
*/
화면에 진입한 상태에서 로그를 보면 3개의 시퀀스가 시작되고있다.
result에 저장된 옵저버블에 새로운 구독자가 추가되면 새로운 시퀀스가 시작된다.
그래서 바인딩 수만큼 새로운 시퀀스가 시작된것이다. 이 상태에서 숫자를 입력하면 3개의 시퀀스가 다시시작된다.
만약 validate대신 네트워크요청을 하는 함수를 호출했다면 동일한 요청이 3번씩 실행된것.
이렇게 되면 시스템 리소스를 너무 많이 소모하기 때문에 수정해야한다.
숫자가 아닌 문자가 전달되면 옵저버블에서 에러이벤트를 전달한다. 코드에서 바인딩에 사용되고있는 text, backgroundcolor, isenable속성은 모두 바인더형식으로 선언되어있다. 그리고 바인더는 에러이벤트를 받지 않는다.
에러이벤트를 바인딩하지 않는다
디버깅환경에서는 크래쉬가 발생하고 런타임환경에서는 에러메시지가 출력된다.
어떤 환경에서든지 더이상 UI는 업데이트되지 않는다
수정이 필요하다.
1. 시퀀스가 필요이상으로 시작되는 문제를 먼저 수정한다.
let result = inputField.rx.text
.flatMapLatest { validateText($0).catchErrorJustReturn(false) }
.share()
share연산자를 추가하면 모든 구독자가 하나의 시퀀스를 공유한다.
catchErrorJustReturn연산자를 사용해서 에러이벤트를 false로 바꾼다.
2. 만약 이너옵저버블이 백그라운드 환경에서 결과를 리턴한다면 UI바인딩이 잘못된 쓰레드에서 실행될 가능성이 있다.
let result = inputField.rx.text
.flatMapLatest {
validateText($0)
.observe(on: MainScheduler.instance)
.catchErrorJustReturn(false) }
.share()
메인스케줄러를 직접 지정해주어야 잠재적인 문제도 방지할 수 있다.
결과
import UIKit
import RxSwift
import RxCocoa
enum ValidationError: Error {
case notANumber
}
class DriverViewController: UIViewController {
let bag = DisposeBag()
@IBOutlet weak var inputField: UITextField!
@IBOutlet weak var resultLabel: UILabel!
@IBOutlet weak var sendButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
//1. 텍스트속성에서 flatMapLatest를 호출하고 클로저에서 validateText를 리턴한다
let result = inputField.rx.text
.flatMapLatest {
validateText($0)
.observe(on: MainScheduler.instance)
.catchErrorJustReturn(false) }
.share()
//3. 옵저버블이 전달하는 Bool을 ok 또는 Error로 바꾸고 레이블에 바인딩
result
.map { $0 ? "Ok" : "Error" }
.bind(to: resultLabel.rx.text)
.disposed(by: bag)
//Bool값을 빨, 파로 바구고 레이블 백그라운드와 바인드
result
.map { $0 ? UIColor.blue : UIColor.red }
.bind(to: resultLabel.rx.backgroundColor)
.disposed(by: bag)
//Bool을 버튼의 활성화상태와 바인드
result
.bind(to: sendButton.rx.isEnabled)
.disposed(by: bag)
}
}
//2. 이 함수는 String?을 파라미터로 받아서 새로운 옵저버블을 리턴한다. 옵저버블이 NE를 통해 방출하는건 Bool
func validateText(_ value: String?) -> Observable<Bool> {
return Observable<Bool>.create { observer in
//시퀀스 실행시점을 파악하기위한 로그
print("== \(value ?? "") Sequence Start ==")
defer {
print("== \(value ?? "") Sequence End ==")
}
//문자열을 Double로 컨버전하고 실패한경우 에러이벤트를 전달
guard let str = value, let _ = Double(str) else {
observer.onError(ValidationError.notANumber)
return Disposables.create()
}
//성공했다면 Next이멘트로 true를 전달하고
observer.onNext(true)
//completed를 전달
observer.onCompleted()
return Disposables.create()
}
}
이전과 달리 새로운 숫자를 입력하면 시퀀스가 하나씩 시작된다.
문자를 입력해도 구독자로 false가 전달되고 구현한 코드에 맞게 UI가 업데이트된다.
에러이벤트로인해 크래쉬가 발생하거나 UI가 더이상 업데이트되지 않는 문제도 해결
지금부터 driver를 사용해서 이 코드를 개선시켜본다.
driver는 우리가 직접 생성하지 않는다.
asDriver메서드를 사용해서 일반 옵저버블을 driver로 변환한다.
let result = inputField.rx.text.asDriver()
.flatMapLatest {
validateText($0)
}
//Driver로 대체
//.observe(on: MainScheduler.instance)
//.catchErrorJustReturn(false)
Driver는 시퀀스를 공유하기 때문에 share연산자는 필요없다.
두 연산자도 드라이버로 대체한다.
asDriver메서드를 보면 에러이벤트가 전달되는 시점에 사용할 기본 값이나 리커버리 옵저버블을 전달할 수 있다.
여기에선 기본값으로 false를 전달한다.
이렇게 되면 아래의 bind연산자에서 에러가 발생한다.
경우에 따라서 map연산자에서 에러가 발생할 수 있는데
bind(to:) 메서드로 인해 발생하는 에러이다.
driver를 사용할 때에는 bind(to:)대신 drive메서드를 사용한다.
실행결과는 이전과 같다.
1. 드라이버는 모든 작업이 메인쓰레드에서 실행되는 것을 보장하기 때문에 스케쥴러를 지정할 필요가 없다.
2. 시퀀스를 공유하기 때문에 불필요한 리소스낭비를 막아준다는 장점이 있다.
3. 동시에 에러처리까지 단순하게 구현할 수 있다.
앞으로 UI바인딩 코드를 작성할 때 드라이버를 적극활용하기
import UIKit
import RxSwift
import RxCocoa
enum ValidationError: Error {
case notANumber
}
class DriverViewController: UIViewController {
let bag = DisposeBag()
@IBOutlet weak var inputField: UITextField!
@IBOutlet weak var resultLabel: UILabel!
@IBOutlet weak var sendButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let result = inputField.rx.text.asDriver()
.flatMapLatest {
validateText($0)
.asDriver(onErrorJustReturn: false)
}
result
.map { $0 ? "Ok" : "Error" }
.drive(resultLabel.rx.text)
.disposed(by: bag)
result
.map { $0 ? UIColor.blue : UIColor.red }
.drive(resultLabel.rx.backgroundColor)
.disposed(by: bag)
result
.drive(sendButton.rx.isEnabled)
.disposed(by: bag)
}
}
func validateText(_ value: String?) -> Observable<Bool> {
return Observable<Bool>.create { observer in
print("== \(value ?? "") Sequence Start ==")
defer {
print("== \(value ?? "") Sequence End ==")
}
guard let str = value, let _ = Double(str) else {
observer.onError(ValidationError.notANumber)
return Disposables.create()
}
observer.onNext(true)
observer.onCompleted()
return Disposables.create()
}
}
signal
Signal와 Driver는 UI 계의 PublishSubject / BehaviorSubject 같은 것...!
Signal과 Driver 의 차이점
- Signal은 새로운 구독자에게 replay 해주지 않는다.
(Driver 처럼 구독하는 순간 초기값이나 최신값을 주지 않는다. 구독한 이후에 발행되는 값을 받음. 위의 사진 참고)
- Signal은 emit함수로 이벤트 처리 / Driver는 drive함수로 이벤트 처리
signal.emit(onNext: { (element) in
}
driver.drive(onNext: { (element) in
}