ios dev kangwook.

9. Beginning RxCocoa 본문

RxSwift

9. Beginning RxCocoa

kangwook 2022. 12. 29. 17:57
  • Rx는 다중 플랫폼 프레임워크
  • RxSwift는 RxPython, RxRuby, RxJS 및 기타 모든 플랫폼이 준수하는 일반 API 디자인을 밀접하게 따르므로 iOS 또는 macOS 용 개발을 지원하는 UIKit 또는 Cocoa 와의 특정 기능이나 통합이 포함되어 있지 않다.
  • RxCocoa는 독립형 라이브러리로, 미리 빌드된 많은 기능을 사용하여 UIKit 및 Cocoa와 더 잘 통합할 수 있다.
  • RxCocoa는 반응형 네트워킹을 수행하고, 사용자 상호 작용에 반응하고, 데이터 모델을 UI 컨트롤에 바인딩하는 등의 작업을 수행할 수 있는 기본 클래스를 제공

 


Getting Started

  • Wundercast라는 iOS 애플리케이션을 예로 프로젝트 시작 → OpenWeatherMap(http://openweathermap.org) 에서 제공하는 현재 날씨 정보를 사용하는 날씨 애플리케이션
  • 이 앱에선 UILabel과 UITextField를 자주 사용
    • UITextField+Rx.swift
      • 50줄 미만의 코드로 매우 짧고 유일한 속성은 ControlProperty<String?> 으로 명명된 text
      • 이 타입은 구독할 수 있고 새 값을 삽입할 수 있는 특수 Subject와 유사한 타입
    • UILabel+Rx.swift
      • text 와 attributetext 라는 속성이 있는데 둘 다 Binder 라는 새로운 타입이 사용
      • 이 ObserverType을 준수하는 객체는 새 값을 수락할 수 있지만 구독할 수 없는 쓰기 전용 엔티티를 원하는 경우 전용의 특수 타입
      • Binder는 UI를 기본 로직과 바인딩 하는데 자주 사용
      • 또 중요한 사실은 오류를 받아들일 수 없다는 점
      • Binder에 오류를 보내면 디버그 모드에서 fatalError() 가 발생하며, 프로덕션 모드에서 앱을 실행할 때 런타임 오류가 로그에 출력된다.

Using RxCocoa with basic UIKit controls

Displaying the data using RxCocoa

  • 현재 ViewController.swift는 이 프로젝트에 존재하는 단 하나의 ViewController
  • 이 프로젝트의 목표는 ViewController.swift에 데이터를 제공할 ApiController.swift를 연결하는 것

  • Observable은 모든 구독자에게 일부 데이터가 도착했음을 알리고 처리할 값을 푸시할 수 있는 엔티티
  • 이러한 이유로 ViewController에서 작업하는 동안 Observable을 구독할 올바른 위치는 viewDidLoad 내부 → 가능한 빨리 구독해야하지만 View가 로드된 후에만 구독해야하기 때문
  • 나중에 구독하면 누락된 이벤트가 발생하거나 데이터를 바인딩하기 전에 UI의 일부가 표시될 수 있다.
  • 따라서 응용 프로그램이 처리하고 사용자에게 표시해야하는 데이터를 생성하거나 요청하기 전에 모든 구독을 생성해야 한다.
private let disposeBag = DisposeBag()
 
override func viewDidLoad() {
    // ...
 
    APIController.shared.currentWeather(city: "RxSwift")
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { data in
        self.tempLabel.text = "\(data.temperature)° C"
        self.iconLabel.text = data.icon
        self.humidityLabel.text = "\(data.humidity)%"
        self.cityNameLabel.text = data.cityName
    })
    .disposed(by: disposeBag)
}

 

 

  • RxCocoa는 Cocoa에 많은 것을 추가하므로 이 기능을 사용하여 궁극적인 목표를 달성할 수 있다.
  • 프레임워크는 프로토콜 확장 기능을 사용하고 많은 UIKit 구성 요소에 rx 네임 스페이스를 추가
  • TextField를 보면 UITextField+Rx.swift에서 확인한 text가 있다.
  • ObserverType을 모두 준수하는 ControlProperty<String?> 인 Observable을 반환하므로 구독하고 여기에 새 값을 추가하여 TextField를 설정할 수 있다.
override func viewDidLoad() {
    // ...
     
    searchCityName.rx.text.orEmpty
        .filter { !$0.isEmpty }
        .flatMap { text in
            return ApiController.shared.currentWeather(city: text)
                .catchErrorJustReturn(ApiController.Weather.empty)
        }
        .observeOn(MainScheduler.instance)
        .subscribe(onNext: { data in
            self.tempLabel.text = "\(data.temperature)° C"
            self.iconLabel.text = data.icon
            self.humidityLabel.text = "\(data.humidity)%"
            self.cityNameLabel.text = data.cityName
        })
        .disposed(by: disposeBag)
}

 

Retrieving data from the Open Weather API

  • API는 구조화 된 JSON 응답을 반환하며 유용한 정보를 가지고 있다.
  • ApiController.swift 내부에는 String을 가져와서 애플리케이션의 현재 날씨를 시각적으로 나타내는 날씨 아이콘의 UTF-8 코드인 또 다른 String을 반환하는 iconNameToChar라는 메소드가 존재
  • 같은 파일에는 네트워크 요청을 생성하는 편리한 메소드인 buildRequest가 존재
  • 이것은 URLSession에 RxCocoa의 래퍼를 사용하여 네트워크 요청을 수행
    • GET(또는 POST) 요청을 올바르게 빌드하기 위해 기본 URL을 가져오고 구성 요소를 추가
    • API 키를 사용
    • 요청의 콘텐츠 유형을 application / json 으로 설정
    • metric 단위로 요청
    • 나중에 currentWeather 메소드에서 JSONDecoder를 사용하여 디코딩 될 Observable 데이터를 반환
  • 마지막 부분은 단일 리턴 라인으로 축소
// ApiController - buildRequest
// ...
return session.rx.data(request: request)
  • 이것은 데이터 메소드를 사용하는 URLSession 주위에 RxCocoa의 rx 확장을 사용
  • 그러면 Observable<Data>가 반환
  • 나중에 JSONDecoder를 사용하여 이 데이터를 디코딩

 

// ApiController currentWeather
func currentWeather(city: String) -> Observable<Weather> {
    return buildRequest(pathComponent: "weather", params: [("q", city)]
        .map { data in
            let decoder = JSONDecoder()
            return try decoder.decode(Weather.self, from: data)
        }
}
  • 이 요청은 Decodable 프로토콜을 준수하기 때문에 Weather 구조체로 디코딩 할 수 있는 Data 객체를 반환
  • 일반적으로 Rx로 작업할 때는 항상 시각화를 사용하는 것이 좋으며, 좀 더 자세한 내용이 포함된 업데이트 된 다이어그램은 ApiController 내부에서 무엇이 작동하는지 이해하는데 도움이 된다.


Binding Observables

  • RxCocoa는 프레임워크에 포함된 몇 가지 유형에만 의존하는 다소 간단한 솔루션을 제공
  • 중요한 점은 RxCocoa에서 바인딩이 단방향 데이터 스트림이라는 점

 

What are binding observables?

  • 바인딩을 이해하는 가장 쉬운 방법은 관계를 두 엔티티 간의 연결로 생각하는 것

  • value를 생산하는 생산자
  • 생산자의 값을 처리하는 소비자
  • 소비자는 값을 반환할 수 없다. → RxSwift의 바인딩을 사용할 때 일반적인 규칙

  • 바인딩의 기본 방법은 bind(to: ) 이며 Observable을 다른 엔티티에 바인딩하는데 사용
  • 소비자는 ObserverType을 준수해야 한다.
  • ObserverType 인 RxSwift와 함께 번들로 제공한 유일한 타입은 실제로 Subject이며, 이는 ObserverType과 ObservableType을 모두 준수하기 때문에 이벤트를 기록할 뿐만 아니라 구독할 수도 있다.
  • bine(to: ) 는 사용자 인터페이스를 기본 데이터에 바인딩하는 것 뿐만 아니라 다른 용도로도 사용할 수 있다.
    • 예를 들어, bind(to: ) 를 사용하여 종속 프로세스를 만들 수 있으므로 특정 Observable은 화면에 아무것도 표시하지 않고 일부 백그라운드 작업을 수행하는 Subject를 트리거 한다.
Note
ObserverType을 준수하는 객체 외에도 Relay에서 bind(to: )를 사용할 수 있다.이러한 bind(to: ) 메소드는 Relay가 실제로 ObserverType을 따르지 않기 때문에 별도의 오버로드
  • 마지막으로 bind(to: )가 실제로 subscribe()의 별칭이라는 점
    • bind(to: )를 호출하면 내부적으로 subscribe(observer)를 호출

 

Using binding observables to display data

  • 첫 번째 변경 사항은 subscribe(onNext: )를 사용하여 데이터를 올바른 UILabel에 할당하는 Observable을 리팩토링
override func viewDidLoad() {
    // ...
 
    let search = searchCityName.rx.text.orEmpty
        .filter { !$0.isEmpty }
        .flatMapLatest { text in
            return ApiController.shared.currentWeather(city: text)
                .catchErrorJustReturn(ApiController.Weather.empty)
        }
        .shared(replay: 1)
        .observeOn(MainScheduler.instance)
}
  • flatMapLatest는 검색 결과를 재사용 가능하게 만들고 일회성 데이터 소스를 multi-use Observable로 변환

  • 이 작은 변경으로 다른 구독의 모든 단일 매개 변수를 처리하여 표시하는데 필요한 값을 매핑할 수 있다.
  • 예를 들어, Observable 공유 데이터 소스에서 온도를 문자열로 가져오는 방법
search.map { "\($0.temperature)° C" }
    .bind(to: tempLabel.rx.text)
    .disposed(by: disposeBag)
 
// 이 후 다른 데이터도 각 Label에 바인딩
search.map { $0.icon }
    .bind(to: iconLabel.rx.text)
    .disposed(by: disposeBag)
 
search.map { "\($0.humidity)%" }
    .bind(to: humidityLabel.rx.text)
    .disposed(by: disposeBag)
 
search.map { $0.cityName }
    .bind(to: cityNameLabel.rx.text)
    .disposed(by: disposeBag)

Improving the code with Traits

  • RxCocoa는 Cocoa 및 UIKit을 쉽게 사용할 수 있도록 더욱 고급 기능을 제공
  • bind(to: ) 외에도 Observable의 특수 구현을 제공하며, 그 중 일부는 UI: Traits 와 함께 사용하기 위해 생성
  • Trait은 클래스 그룹으로, 특히 UI로 작업할 때 간단하고 쓰기 쉬운 코드를 만드는데 도움이 되는 특수 Observable 항목

 

What are ControlProperty and Driver?

  • Trait은 공식 문서에서 다음과 같이 설명
    • 인터페이스 경계를 넘어 통신하고 Observable 시퀀스 속성을 보장
  • 처음에는 혼란스러울 수 있지만 RxCocoa의 특성을 사용하는 규칙은 전체 개념을 조금 더 이해하기 쉽게 만든다. 규칙은 다음과 같다.
    • 에러를 내보낼 수 없다.
    • MainScheduler에서 관찰된다.
    • MainScheduler에서 subscribe
    • Signal을 제외하고 리소스를 공유
  • 이러한 엔티티는 무언가가 항상 사용자 인터페이스에 표시되고 항상 사용자 인터페이스에서 처리할 수 있도록 한다.
  • RxCocoa의 Traits
    • ControlProperty and ControlEvent
      • ControlProperty → 기존에 사용하던 rx 네임 스페이스를 사용하여 데이터를 올바른 사용자 인터페이스 구성요소에 바인딩 할 때 사용
      • ControlEvent → TextField를 편집하는 동안 키보드의 "Return" 버튼을 누르는 것과 같은 UI 구성요소의 특정 이벤트를 수신하는데 사용
    • Driver → 동일한 제약 조건을 가진 특수 관찰이 가능하므로 오류가 발생하지 않는다. 모든 프로세스가 메인 스레드에서 실행되도록 보장하여 백그라운드 스레드에서 UI 변경을 방지
    • Signal → 리소스를 공유하지 않는다는 점을 제외하고 Driver와 똑같은 속성을 공유하므로 구독자에게 아이템을 재생하지 않는다.

 

  • 일반적으로 Trait은 프레임워크의 선택적 부분 → 강제로 사용하지 않아도 된다.
  • 적절한 스케줄러에서 올바른 작업을 만들고 있는지 확인하면서 Observable 항목과 Subject를 자유롭게 사용해도 된다.
  • 그러나 컴파일을 하는 동안 몇 가지 검사와 UI를 처리할 때 예측 가능한 규칙이 필요한 경우 이러한 구성요소는 매우 강력하고 시간을 절약할 수 있다.

 

Improving the project with Driver and ControlProperty

  • 이러한 멋진 개념을 응용 프로그램에 적용하고 모든 작업이 올바른 스레드에서 수행되는지 확인하고 오류가 발생하지 않고 구독이 결과를 제공하지 못하도록 해야한다.
  • 첫 번째 단계는 Observable 날씨 데이터를 Driver로 변환하는 것
let search = searchCityName.rx.text.orEmpty
    .filter { !$0.isEmpty }
    .flatMapLatest { text in
        return ApiController.shared.currentWeather(city: text)
            .catchErrorJustReturn(ApiController.Weather.empty)
    }
    .asDriver(onErrorJustReturn: ApiController.Weather.empty)       // Observable을 Driver로 변환하는 방법.
                                                                    // onErrorJustReturn 매개 변수는 Observable 오류가 발생하는 경우 사용할 기본값을 지정하여 Driver 자체에서 오류를 발생시킬 가능성을 제거  
}
  • asDriver(onErrorDriverWith: ) → 이 메소드를 사용하면 오류를 수동으로 처리할 수 있다. 이 용도로만 생성된 새 Driver를 반환
  • asDriver(onErrorRecover: ) → 다른 기존 Driver와 함께 사용할 수 있다. 방금 오류가 발생한 현재 Driver를 복구하기 위해 작동

 

// UI update with drive
search.map { "\($0.temperature)° C" }
    .drive(tempLabel.rx.text)
    .disposed(by: disposeBag)
 
search.map { $0.icon }
    .drive(iconLabel.rx.text)
    .disposed(by: disposeBag)
 
search.map { "\($0.humidity)%" }
    .drive(humidityLabel.rx.text)
    .disposed(by: disposeBag)
 
search.map { $0.cityName }
    .drive(cityNameLabel.rx.text)
    .disposed(by: disposeBag)
  • 이렇게 하면 Driver의 성능을 활용하면서 애플리케이션의 올바른 UI 동작을 복원
  • drive() 는 bind(to: ) 보다 RxCocoa의 Trait을 사용하는 동안 의도를 더 잘 표현

 

  • RxCocoa의 많은 부분을 활용했지만 여전히 개선할 수 있는 부분이 있다.
  • 응용 프로그램은 문자를 입력할 때마다 요청을 실행하기 때문에 너무 많은 리소스를 사용하고 너무 많은 API 요청을 만든다.
// replace search

// 기존
let search = searchCityName.rx.text.orEmpty
 
// 변경
let search = searchCityName.rx.controlEvent(.editingDidEndOnExit)
    .map { self.searchCityName.text ?? "" }
  • 이렇게 변경하면 사용자가 "검색" 버튼을 누를 때만 날씨를 검색
  • 불필요한 네트워크 요청을 하지 않고 코드는 Traits에 의해 컴파일 타임에 제어된다.
  • 또한 currentWeather(city: )에서 반환된 Observable에 대한 catchErrorJustReturn() 호출을 제거

 

 

  • 원래 스키마는 전체 UI를 업데이트 한 단일 Observable 항목을 사용
  • 여러 블록의 분석을 통해 subscribe에서 bindTo로 전환하고 ViewController 전체에서 동일한 Observable을 재사용
  • 이 접근 방식은 코드를 재사용하므로 매우 작업하기 쉽게 만든다.
  • 예를 들어, 현재 기압을 사용자 인터페이스에 추가할 경우 속성을 구조체에 추가한 후 다른 UILabel을 추가하고 해당 속성을 매핑하기만 하면 된다.

Disposing with RxCocoa

  • 기본 ViewController가 할당 해제될 때 모든 구독을 처리하는 DisposeBag이 존재.
  • 그러나 이 예에서 모든 클로저에 weak 또는 unowned 가 사용되지 않음.
  • 그 이유는 이 예의 경우 단일 ViewController이며 MainViewController는 애플리케이션이 실행되는 동안 항상 화면에 표시되므로 메모리를 방지할 필요가 없다.

 

Unowned VS weak with RxCocoa

  • weak 과 unowned 를 사용하는 규칙은 일반 Swift 클로저를 사용할 때 따르는 것과 동일하며 subscribe(onNext: ) 와 같은 Rx의 클로저 변형을 호출할 때 주로 관련
  • 클로저가 @escaping 클로저인 경우 항상 weak 또는 unowned 클로저를 사용하는 것이 좋다.
  • 그렇지 않으면 retain cycle 이 생기고 구독이 해제되지 않는다.
  • weak을 사용하면 self에 대한 optional 참조가 제공되고 unowned를 사용하면 self에 대한 non-optional 참조가 제공된다.
  • unowned 는 self!를 제공하므로 주의해야 한다.

 

 


Summary of RxSwift and RxCocoa

 

'RxSwift' 카테고리의 다른 글

8. Time-Based Operators  (0) 2022.12.27
7. Combining Operators  (1) 2022.12.23
6. Transforming Operators  (0) 2022.12.22
5. Filtering Operators  (0) 2022.12.19
4. Observables & Subjects in Practice  (1) 2022.12.13
Comments