-
맨 처음 MS에서 만들어진 rx는 위처럼 비동기 프로그래밍을 위한 API다.
Async 한걸 아래처럼 작성하는데, 이걸 간결하게 하고 싶다. 하면 사용한다고 아주 쉽게 이해하면 된다. (콜백 지옥속에서 처리 과정을 찾기 어려울때 이걸 확인해야함다)
* 우선 본 내용은 https://www.youtube.com/watch?v=w5Qmie-GbiA 보고 공부차 정리형식으로 작성되었습니다.
Why?
이미지 처리할때 보통 아래와 같은 방법으로 비동기 통신의 처리를 하고 있습니다.
// 이미지 처리할때 예제 DispatchQueue.global(qos: .default).async { [weak self] in if let url: URL = URL.init(string: url) { // URL 셋팅 if let data = try? Data.init(contentsOf: url, options: []) { // data 셋팅 if let image = UIImage.init(data: data) { DispatchQueue.main.async { self?.image = image } } } } }
하지만 위의 비동기처리에서 데이터를 받아오다가 취소를 하고 싶으면 어떻게 할까? 쉽사리 방법이 떠오르지 않을것이다.(비트를 세운다ㅏ..?)
그래서 다음과 같은 rxSwift를 사용할수 있다
var disposable: Disposable? var disposeBag: DisposeBag? // 여러개를 담을수도 있음 // MARK: - IBAction /// 이미지 로드 예제 @IBAction func onLoadImage(_ sender: Any) { imageView.image = nil rxswiftLoadImage(from: LARGER_IMAGE_URL) .observeOn(MainScheduler.instance) .subscribe({ result in switch result { case let .next(image): self.imageView.image = image case let .error(err): print(err.localizedDescription) case .completed: break } }) .disposed(by: disposeBag) } func rxswiftLoadImage(from imageUrl: String) -> Observable<UIImage?> { return Observable.create { seal in // 추후 간소화 예정 just asyncLoadImage(from: imageUrl) { image in seal.onNext(image) seal.onCompleted() } return Disposables.create() } } /// 취소 @IBAction func onCancel(_ sender: Any) { disposeBag = DisposeBag() // 취소 시킨다. // disposable?.dispose() 이런 방법도 있음 }
이것만으로도 rxSwift가 왜 필요한지 알 수 있었다. 물론 현재 위의 코드가 복잡해 보인다는 느낌이 들 수 있다. 취소 처리가 들어가기도 했고 아직 Operators을 제대로 사용하지 않아서 인데 아래에서 그 의문이 풀려가길 바란다...!
쓰는 이유 결론 ?
- 강력한 비동기 처리
- 간결해지는 코드, 가독성 향상
Observable
무려 한국어로 제공해준다. http://reactivex.io/documentation/ko/observable.html
Operators
// operators 예시 func justTest() { Observable.just(["Hello", 1]) // just는 그대로 내린다. .subscribe(onNext: { arr in print(arr) // [Hello, 1] }) .disposed(by: disposeBag) } func fromTest() { // from은 하나씩 읽어 내려간다고 보면 됨 Observable.from(["with", "곰튀김"]) // ... 연결하는걸 stream 이라고 함 .map { $0.count } .subscribe(onNext: { str in print(str) }) .disposed(by: disposeBag) }
이미지를 operator를 사용했을때 (아직 concurrent 아님)
func imageTest() { Observable.just("800x600") .map { $0.replacingOccurrences(of: "x", with: "/") } .map { "https://picsum.photos/\($0)/?random" } .map { URL(string: $0) } .filter { $0 != nil } .map { $0! } .map { try Data(contentsOf: $0) } .map { UIImage(data: $0) } .subscribe(onNext: { image in self.imageView.image = image }) .disposed(by: disposeBag) }
결과 처리는 어떤식으로 할까요? subscribe의 종류는 아래와 같습니다.
// case 로 나누기 func subscribeTest() { Observable.just(["11","22"]) .subscribe({ event in switch event { case .next: // 다음으로 갈때 실행 let text = event print(text) case let .error(err): // 에러가 났을때 print(err.localizedDescription) case .completed: // 성공으로 모두 끝났을때 break } }) .disposed(by: disposeBag) // error 또는 completed } // 위의 case와 같지만 모두 구현하고 싶지 않을때 아래처럼 쓸 수 있다. func subscribeTest() { Observable.just(["11","22"]) .subscribe(onNext: { s in print(s) }, onError: { err in print(err.localizedDescription) }, onDisposed: { print("disposed") }) .disposed(by: disposeBag) } // completed는 구현하지 않음 // onNext안에 처리 내용이 길어지면 어떨까요? func longTest() { Observable.just(["11",22]) .subscribe(onNext: output) .disposed(by: disposeBag) } func output(_ s: Any) -> Void { print(s) }
Scheduler
func imageTest() { Observable.just("800x600") .observeOn(ConcurrentDispatchQueueScheduler(qos: .default)) // concurrent .map { $0.replacingOccurrences(of: "x", with: "/") } .map { "https://picsum.photos/\($0)/?random" } .map { URL(string: $0) } .filter { $0 != nil } .map { $0! } .map { try Data(contentsOf: $0) } .map { UIImage(data: $0) } .observeOn(MainScheduler.instance) // main에서 하게끔 중요 .subscribe(onNext: { image in self.imageView.image = image }) .disposed(by: disposeBag) } // 아래처럼 할수도 있다. func imageTest() { Observable.just("800x600") .map { $0.replacingOccurrences(of: "x", with: "/") } .map { "https://picsum.photos/\($0)/?random" } .map { URL(string: $0) } .filter { $0 != nil } .map { $0! } .map { try Data(contentsOf: $0) } .map { UIImage(data: $0) } .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .default)) // subscribe 할때 부터라 위치 상관 없음 .observeOn(MainScheduler.instance) // main에서 하게끔 중요 .subscribe(onNext: { image in self.imageView.image = image }) .disposed(by: disposeBag) }
do가 뭐지?
여기서 살짝 하나 추가 ++ Driver
항상 UI는 main에서 처리해야하는데 (.observeOn(MainScheduler.instance)
아래 driver를 사용할 수 있다.
private func bindOutput() { let idEvent = idField.rx.text.orEmpty.asDriver() let valid = idEvent.map(checkEmailValid) valid .drive(onNext: {b in self.idValidView.isHidden = b }) .disposed(by: disposeBag) }
RxCocoa
pod 'RxCocoa' : UI를 처리하기 위한거라고 보면 됨
Subject
4가지가 있습니다. 각각 subscribe 한 애한테 어떤식으로 전달하는지 보면됩니다.
BehaviorSubject : subscribe 하면 이전에 있던 값 주고 발생하면 또 알려주고
PublishSubject : Behavior 와 비슷하지만, subscribe 했다고 일이 일어나는게 아니고, observable에 일어나야 전달
ReplaySubject : 말그대로 subscribe 하면 다시 한번, history를 전달한다.
AsyncSubject : Completed 즉 끝나야만 값을 전달받고 마지막 결과만 받는다.
위에서 배운 내용을 한번에 이해해 보겠다.
더보기예제
// UI 코드가 있다고 생각 // TextField Delegate를 상속 받지 않고 아래처럼 작성할 수 있다. idField.rx.text.orEmpty // optional 해제라고 보면 된다. .map(checkEmailValid) .subscribe(onNext: { b in self.idValidView.isHidden = b }) .disposed(by: disposeBag) pwField.rx.text.orEmpty .map(checkPasswordValid) .subscribe(onNext: { b in self.pwValidView.isHidden = b }) .disposed(by: disposeBag) Observable.combineLatest( // 뭐하나라도 바뀌면 실행해줌 (둘다 바뀌어야 실행된다? 찝 / 머지? 들어오는대로 순서대로 내려보낸다) idField.rx.text.orEmpty.map(checkEmailValid), pwField.rx.text.orEmpty.map(checkPasswordValid), resultSelector: { s1, s2 in s1 && s2 } ) .subscribe(onNext: { b in self.loginButton.isEnabled = b }) .disposed(by: disposeBag) // 하지만 위의 코드는 계속해서 orEmpty 하는등 중복되는 코드가 있다. 이는 아래처럼 수정할 수 있다. let idInputOb : Observable<String> = idField.rx.text.orEmpty.asObservable() let pwInputOb : Observable<String> = pwField.rx.text.orEmpty.asObservable() let idValidOb = idInputOb.map(checkEmailValid) let pwValidOb = pwInputOb.map(checkPasswordValid) idValidOb.subscribe(onNext: { b in self.idValidView.isHidden = b }) .disposed(by: disposeBag) pwValidOb.subscribe(onNext: { b in self.pwValidView.isHidden = b }) .disposed(by: disposeBag) Observable.combineLatest(idValidOb, pwValidOb, resultSelector: { $0 && $1 }) .subscribe(onNext: {b in self.loginButton.isEnabled = b }) .disposed(by: disposeBag) // Subject의 개념을 적용해볼까요? // 함수 바깥에 선언 let idValid: BehaviorSubject<Bool> = BehaviorSubject(value: false) let pwValid: BehaviorSubject<Bool> = BehaviorSubject(value: false) // 통로를 만들어 놓았다. // 아래의 두가지 방법으로 가능 idValidOb.subscribe(onNext: { b in self.idValid.onNext(b) }) idValidOb.bind(to: idValid) // idValidOb 무슨일이 생기면 idValid로 보내준다. bind라는걸 사용!!! // 실제로 사용할때! let idInputOb : Observable<String> = idField.rx.text.orEmpty.asObservable() idInputOb.map(checkEmailValid).bind(to: idValid).disposed(by: disposeBag) // 위의 코드를 아래처럼 밖으로 선언해서 정리할 수 있습니다~ let idValid: BehaviorSubject<Bool> = BehaviorSubject(value: false) let pwValid: BehaviorSubject<Bool> = BehaviorSubject(value: false) // 통로를 만들어 놓았다. let idInputText: BehaviorSubject<String> = BehaviorSubject(value: "") let pwInputText: BehaviorSubject<String> = BehaviorSubject(value: "") // MARK: - Bind UI private func bindInput() { // input : 아이디 입력, 비번 입력 idField.rx.text.orEmpty .bind(to: idInputText) .disposed(by: disposeBag) pwField.rx.text.orEmpty .bind(to: pwInputText) .disposed(by: disposeBag) idInputText .map(checkEmailValid) .bind(to: idValid) .disposed(by: disposeBag) pwInputText .map(checkEmailValid) .bind(to: pwValid) .disposed(by: disposeBag) } private func bindOutput() { // output : view 변경 idValid.subscribe(onNext: { b in self.idValidView.isHidden = b }) .disposed(by: disposeBag) pwValid.subscribe(onNext: { b in self.pwValidView.isHidden = b }) .disposed(by: disposeBag) Observable.combineLatest(idValid, pwValid, resultSelector: { $0 && $1 }) .subscribe(onNext: {b in self.loginButton.isEnabled = b }) .disposed(by: disposeBag) } // 위의 예제들은 completion에 self.button 이런식으로 reference count 를 증가시켰는데, complete 되지 않습니다.(항상 input을 기다리죠?) 그럼 navigation Controller에서 view 가 내려가도 // disposeBag이 ㅁㅔ모리에서 해제되지 않습니다. 이는 swift와 동일한 방식으로 아래처럼 하면 됩니다! // 아주 간단하게는 viewWillDisappear에 disposebag = DisposeBag() 하면 됨 private func bindOutput() { // output : view 변경 idValid.subscribe(onNext: {[weak self] b in self?.idValidView.isHidden = b }) .disposed(by: disposeBag) } // 위의 방법도 귀찮죠? 곰튀김의 클로져와 메모리 해제 실험 읽어보기
RxSwift의 확장
https://github.com/RxSwiftCommunity
Reactor Kit
: 프레임 워크
view -> action -> Reactor -> State -> View (단방향)
이걸 사용하니까 어떨까요?
ReactorKit에서 권장하는 방식에따라 코드 작성 가능
단위 테스트에도 활용하기 좋다고 합니다.
출처 : https://www.youtube.com/watch?v=G1b1sBy8XBA
++
RxOptional
: https://github.com/RxSwiftCommunity/RxOptional
.filterNil()
RxViewController
: https://github.com/devxoul/RxViewController
예시로 들어주신게 viewWillAppear에서 한번만 실행하고 싶을때 우리는 flag를 세워서 하는 방법이 생각날텐데
viewWillAppear
.take(: 1) // 이런 방식으로 처리할 수 있다고 합니다.
728x90'RxSwift' 카테고리의 다른 글
RxSwift 스터디 계획 따라가기 (0) 2021.07.12 RxSwift Subject (0) 2021.07.09 Rxswift Debounce / Throttle (0) 2021.06.29 Rxswift (map, flatmap, compactmap) 정리 (0) 2021.06.28 Rxswift (flatMap, flatMapFirst, flatMapLatest) (0) 2021.06.24