ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RxSwift
    RxSwift 2021. 6. 17. 17:48

    http://reactivex.io/

    맨 처음 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

     

    ReactiveX - Observable

    Observable ReactiveX에서 옵저버는 Observable을 구독한다. Obseravable이 배출하는 하나 또는 연속된 항목에 옵저버는 반응한다. 이러한 패턴은 동시성 연산을 가능하게 한다. 그 이유는 Observable이 객체를

    reactivex.io

     


    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

     

    RxSwift Community

    RxSwift ecosystem projects. RxSwift Community has 73 repositories available. Follow their code on GitHub.

    github.com


    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

    댓글

Designed by Tistory.