Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beginning RxCocoa #24

Open
simoniful opened this issue Apr 30, 2022 · 0 comments
Open

Beginning RxCocoa #24

simoniful opened this issue Apr 30, 2022 · 0 comments
Labels

Comments

@simoniful
Copy link
Owner

simoniful commented Apr 30, 2022

Ch.12 Beginning RxCocoa

A. 기본 UIKit 조작과 RxCocoa 사용

1. RxCocoa를 사용해서 데이터 표시하기

  • 더미테이터의 구성과 전달에 있어서 Observable 사용
  • Model에서의 데이터 패칭과 단방향적 데이터 활용
  • View를 구성해야하는 곳에서의 호출과 데이터 바인딩을 위한 구독
    • View 구성 객체의 rx 키워드를 사용한 ControlProperty 접근과 해당 프로퍼티를 통한 구독, 이벤트 방출 모두 가능
  • View 구성에 있어서 Main scheduler 주의 - bind, drive 등 trait 사용으로 보완 필요

2. OpenWeather API에서 데이터 가져오기

  • URLSession을 wrapper하여 활용, Observable를 반환
  • 데이터 활용에 있어서 flatMap() 사용
  • catchErrorJustReturn 연산자
    • API에서 받은 에러를 통해 observable이 dispose 되는 것을 막는 역할
    • 데이터 패칭을 통한 에러를 받아도 작업을 멈추지 말고 빈 값을 방출하도록 조치

B. observable 바인딩하기

RxCocoa에서의 바인딩은 단방향 데이터 스트림이다.

  • Producer(Observable)는 값을 만들어낸다.
  • Receiver(Observer)는 만들어진 값을 수신하고 처리한다.

1. observable 바인딩이란?

  • bind(to:)
    • 값을 만들어내고 수동적으로 구독될 수 있는 대상을 서로 연결한다. (textField.rx.text - BehaviorRelay)
    • subscribe()의 특별 맞춤 버전이다. bind(to:)는 호출되었을 때 부수작용이 없다

2. 데이터 표시를 위한 observable 바인딩

 	// 1
     let search = searchCityName.rx.text
         .filter { ($0 ?? "").characters.count > 0 }
         .flatMapLatest { text in
             return ApiController.shared.currentWeather(city: text ?? "Error")
                 .catchErrorJustReturn(ApiController.Weather.empty)
         }
         .share(replay: 1)
         .observeOn(MainScheduler.instance)

     // 2
     search.map { "\($0.temperature)" }
     	// 3
         .bind(to: tempLabel.rx.text)
         .disposed(by: bag)

     // 3
     search.map { "\($0.humidity)%" }
         .bind(to: humidityLabel.rx.text)
         .disposed(by: bag)

     search.map { $0.cityName }
         .bind(to: cityNameLabel.rx.text)
         .disposed(by: bag)

     search.map { $0.icon }
         .bind(to: iconLabel.rx.text)
         .disposed(by: bag)
  • flatMapLatest 부분을 변경하므로써, 검색 결과는 재사용 될 수 있으며, 일회용 데이터 소스를 여러번 사용되는 Observable로 변형
  • 확인할 수 있는 것은 observable이 Rx의 재사용 가능한 속성이 될 수 있다는 것
  • 가독성이 낮은 일회성 코드들을 가독성이 높은 재사용 가능한 코드로 전환

C. Traits를 이용한 코드 개선

Trait은 UI에 특화된 observable 항목의 특수한 구현을 제공
직관적이고 작성하기 쉬운 코드로 이해 가능

1. ControlProperty와 Driver란

Trait은 에러를 방출할 수 없다.
Trait은 메인 스케줄러에서 관찰한다.
Trait은 메인 스케줄러에서 구독한다.
Trait은 부수작용을 공유한다.

  • ControlProperty: 데이터와 유저인터페이스를 연결할 때 rx extension을 통해 사용한 적이 있다.
  • ControlEvent: 텍스트필드에서 글자를 입력할 때 리턴버튼을 누르는 것과 같이, UI구성요소에서의 확실한 이벤트를 듣기위해 사용한다. ControlEvent는 구성요소가 UIControlEvents를 현재 상태에 계속 두고 사용할 때 사용 가능하다.
  • Driver: 에러를 방출하지 않는 특별한 observable이다. 모든 과정은 UI 변경이 background 쓰레드에서 이뤄지는 것을 방지하기 위해 메인 쓰레드에서 이뤄진다.

2. Driver와 ControlProperty을 이용한 코드 개선

let search = searchCityName.rx.text
 	.filter { ($0 ?? "").characters.count > 0 }
 	.flatMapLatest { text in
 		return ApiController.shared.currentWeather(city: text ?? "Error")
 			.catchErrorJustReturn(ApiController.Weather.empty)
 		}
 	.asDriver(onErrorJustReturn: ApiController.Weather.empty)

search.map { "\($0.temperature)" }
    .drive(tempLabel.rx.text)
    .disposed(by: bag)

search.map { "\($0.humidity)%" }
    .drive(humidityLabel.rx.text)
    .disposed(by: bag)

search.map { $0.cityName }
    .drive(cityNameLabel.rx.text)
    .disposed(by: bag)

search.map { $0.icon) }
    .drive(iconLabel.rx.text)
    .disposed(by: bag)
  • 가장 마지막줄의 .asDriver(onErrorJustReturn: ApiController.Weather.empty)를 통해 기존의 observable은 Driver로 전환
  • onErrorJustReturn 파라미터는 observable이 에러를 방출할 때 어떻게 할 것인지 기본값을 정의하고 있다. 그러므로 driver는 스스로 방출된 에러를 떼어내는게 가능하다.
  • bind()를 drive()로 변경하면서 구독하게 할 수 있다.
 let search = searchCityName.rx.controlEvent(.editingDidEndOnExit).asObservable()
         .map { self.searchCityName.text }
         // 2
         .flatMap { text in
             return ApiController.shared.currentWeather(city: text ?? "Error")
         }
         .asDriver(onErrorJustReturn: ApiController.Weather.empty)
  • 상기 코드대로 작성한다면 글자를 입력할 떄 마다 데이터 패칭이 이루어 진다
  • 딜리게이트 패턴에서 활용한 것 처럼, editingDidEndOnExit 이벤트가 발생할 때 패칭을 처리할 수 있다
  • 이렇게하면 입력값은 반드시 유효하기에 에러 처리에 있어서도 currentWeather(city:)를 통해 반환된 observable에 대한 catchErrorJustReturn() 호출을 삭제할 수 있다.

D. RxCocoa와 dispose 하기

구독하는 내부에서 view와 관련하여 부수효과를 발생시킬 때, 항상 weak self로 캡쳐링 하여 구성하는 것이 권장된다.

E. Loading 관련 뷰 구성

1. UIActivityIndicatorView

  • UIActivityIndicatorView는 UIKit에서 가장 흔하게 쓰는 녀석이다. 이 extension에는 다음과 같은 객체가 있다.

     public var isAnimating: UIBindingObserver<Base, Bool>
  • 명칭에서 확인할 수 있듯이, 이 녀석은 isAnimating 객체와 관련있다. 마치 UILabel에서처럼 이 객체는 UIBindingObserver타입으로 결과는 observable 객체에 바인딩되어 background 활동에 대해 통지할 수 있다.

2. UIProgressView

  • UIProgressView는 조금은 덜 일반적인 녀석이지만, 어쨌든 RxCocoa에서 아래와 같은 객체를 제공한다.

     public var progress: UIBindingObserver<Base, Float>
  • UIProgressBar 또한 다른 놈들과 마찬가지로 observable에 바인딩 될 수 있다. 예를 들어 uploadFile() 같은 함수가 있고, 이 놈은 파일을 서버에 업로딩하는 작업을 하면서 동시에 전체 byte에서 얼마만큼의 byte를 업로딩 했는지 알려주는 는 observable을 만들고 있다고 생각해보자. 아래와 같이 표현될 수 있을 것이다.

     let progressBar = UIProgressBar()
     let uploadFileObs = uploadFile(data: fileData)
     uploadFileObs.map { sent, totalToSend in
     	return sent / totalToSend
     }
     	.bind(to:progressBar.rx.progress)
     	.disposed(by:bag)
    • 결과는 progress bar가 값이 제공될 때마다 업데이트 되는 형식으로 나타날 것이다.
    • 또한 사용자는 업로드 과정을 눈으로 확인할 수 있는 지표를 확인할 수 있을 것이다.
@simoniful simoniful added the Rx label May 2, 2022
@simoniful simoniful changed the title RxCocoa Beginning RxCocoa May 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant