Skip to content

Latest commit

 

History

History
435 lines (338 loc) · 29.1 KB

Ch12. Beginning RxCocoa.md

File metadata and controls

435 lines (338 loc) · 29.1 KB

Ch.12 Beginning RxCocoa

A. 시작하기

  • 지금까지 다룬 내용들은 RxSwift의 기초다. 아주 기본적인 부분으로, observable을 어떻게 생성하고, 구독하고 dispose 시키는지에 대해 배운 것이다.

  • 여기서는 RxSwift가 아닌 다른 프레임워크를 소개할 것이다. 바로 RxCocoa다.

  • RxCocoa는 iOS(iPhone, iPad, Apple Watch), Apple TV, macOS 모두에 적용가능하다.

  • 이 장에서는 RxCocoa에 대한 빠른 이해를 위해 OpenWeatherMap API를 이용한 날씨 앱을 만들어볼 것이다. 예제 프로젝트 Wundercast에 적용된 라이브러리는 RxSwift, RxCocoa, SwiftyJSON이다.

    • OpenWeatherMap은 API 키를 필요로 하므로, 다음을 확인할 것: 여기서 가입하세요
    • 회원가입을 완료했으면 API key 페이지로 가서 새 키를 생성할 것: 여기서 생성하세요
    • 만든 API key를 복사하여 ApiController.swift 의 다음과 같은 장소에 붙여넣기 하세요.
     private let apiKey = "[YOUR KEY]"

B. 기본 UIKit 조작과 RxCocoa 사용

  • 앞선 작업을 통해, 어떤 데이터를 입력하고 API를 통해 온도, 습도, 도시명을 함께 받을 준비가 되었다.

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

  • 만약 제공된 프로젝트를 실행해봤다면, 앱이 어떻게 API를 통해 데이터를 검색해서 결과를 가져오기도 전에 데이터를 표시하고 있는지 궁금할 것이다.

  • 방법은 간단하다. 수동으로 입력한 데이터가 올바른지 확인할 수 있기 때문이다. 따라서 어떤 작업이 실패하면 API 처리 코드 어딘가에 있는지 확인되고, 이 것은 Rx의 로직이나 UI관련 코드가 아니라는 것을 알 수 있을 것이다.

  • ApiController.swift에서, 맞는 JSON 데이터를 매핑할 데이터모델로 쓰일 struct를 확인할 수 있다.

  • 역시 ApiController.swift에서 다음 함수를 살펴보자.

       func currentWeather(city: String) -> Observable<Weather> {
         // Placeholder call
         return Observable.just(Weather(cityName: city,
                                        temperature: 20,
                                        humidity: 90,
                                        icon: iconNameToChar(icon: "01d")))
       }
    • 이 함수는 RxCity라는 이름의 가짜 도시명과 더미 데이터를 리턴한다. 이들은 실제 데이터를 서버로부터 받기 전까지 표시될 것이다.
      • 더미 데이터는 인터넷 연결이 없이도 개발 과정을 간소화하고 실제 데이터 구조처럼 작동해볼 수 있도록 도와준다.
  • ViewController.swift를 확인해보자. 이 프로젝트에서는 하나의 뷰컨트롤러만이 표시된다. 여기서의 목적은 하나의 뷰컨트롤러를 ApiController와 연결시켜서 데이터를 제공하게 하는 것이다. 결과적으로는 아래 그림과 같이 단방향 데이터 흐름을 보이게 된다.

  • observable은 데이터를 수신하고 모든 가입자에게 일련의 데이터가 도착했음을 알린다. 또한 처리할 값을 푸시할 수 있다.

  • 따라서 뷰컨트롤러가 작업할 동안 observable을 구독할 적합한 장소는 viewDidLoad다. 이건 뷰가 로드되자마자 구독을 최대한 빨리 할 필요가 있기 때문이다.

  • 구독이 늦어지면 이벤트 일부를 놓치거나, UI의 일부분이 데이터를 바인드하기 전에 보여질 수 있다.

  • 그러므로 앱에서 처리되고 사용자에게 표시되어야 하는 데이터를 만들거나 요청하기 전에 모든 구독을 만들어주어야 한다.

  • 데이터를 가져오기 위해 하기의 코드를 viewDidLoad에 입력합시다.

         ApiController.shared.currentWeather(city: "RxSwift")
             .observeOn(MainScheduler.instance)
             .subscribe(onNext: { data in
                 self.tempLabel.text = "\(data.temperature)"
                 self.iconLabel.text = data.icon
                 self.humidityLabel.text = "\(data.humidity)%"
                 self.cityNameLabel.text = data.cityName
             })
    • 여기까지 작성하면 완료된 모습은 다음과 같다

  • 앱은 더미데이터를 잘 표시하고 있다. 하지만 여기엔 두가지 문제가 있다.

      1. 컴파일러 경고(노란색)가 뜬다. Result of call to 'subscribe(onNext:onError:onCompleted:onDisposed:) is unused'
      1. 텍스트필드 입력부분에 대한 구현이 없다.
  • 하나씩 문제를 풀어보자. 먼저 첫번째 컴파일러 경고를 확인해보자. 이 경우는 뷰컨트롤러가 dismiss 되었을 때 구독을 취소하도록, 구독이 disposable 객체를 리턴하도록 해야한다. 이를 위해 하기의 코드를 입력하자.

         let bag = DisposeBag()
    
         ApiController.shared.currentWeather(city: "RxSwift")
             .observeOn(MainScheduler.instance)
             .subscribe(onNext: { data in
                 self.tempLabel.text = "\(data.temperature)"
                 self.iconLabel.text = data.icon
                 self.humidityLabel.text = "\(data.humidity)%"
                 self.cityNameLabel.text = data.cityName
             })
             .disposed(by: bag)
    • 이는 뷰컨트롤러의 릴리즈 여부에 따라 구독을 취소/dispose 하게 된다. 이는 리소스 낭비를 막아줄 뿐만 아니라 예측하지 못했던 이벤트 발생 또는 다른 부수작용들이 구독을 dispose 하지 않아 발생되지 않도록 막아준다.

    • 두번째 문제를 확인해보자. RxCocoa는 Cocoa 프레임워크 위에서 관련 기능들을 모두 구현할 수 있게 해준다. 프레임워크는 rx 키워드를 추가함으로서 UIKit들을 사용할 수 있게 한다. 즉, 이 문제에서는 serachCityName.rx.를 통해 아래 그림과 같이 사용가능한 메소드를 확인할 수 있다.

      • 앞서 사용한 메소드들이 눈에 띈다. textControlPropert<String?>라는, ObservableType이면서 동시에 ObserverType인 Observable을 리턴한다. 즉, 이를 통해 구독 및 새로운 이벤트 방출 모두 가능하다. _

ControlProperty

  • 하기 코드를 viewDidLoad()에 추가하자.

         // 1
         searchCityName.rx.text
             .filter { ($0 ?? "").characters.count > 0 }
             .flatMapLatest { text in
                 return ApiController.shared.currentWeather(city: text ?? "Error")
                     .catchErrorJustReturn(ApiController.Weather.empty)
         }
             // 2
             .observeOn(MainScheduler.instance)
             .subscribe(onNext: {data in
                 self.tempLabel.text = "\(data.temperature)"
                 self.iconLabel.text = data.icon
                 self.humidityLabel.text = "\(data.humidity)%"
                 self.cityNameLabel.text = data.cityName
             })
             .disposed(by: bag)
    • 주석을 따라 확인해보자.
        1. 상단의 코드는 표시할 데이터들과 함께 새로운 observable을 리턴한다.
        • currentWeathernil이나 빈 값을 허용하지 않으므로, 이들을 걸러낸다.
        • 그리고 ApiController 클래스를 이용하여 날씨 데이터들을 받아온다.
        1. 이 작업을 적절한 쓰레드로 이동시키고 데이터를 표시하기 위한 코드다
        • 일단 MainScheduler로 이동한 이상, 현재 날씨 데이터들은 UI와 함께 업데이트 되어야 한다. 하기 그림은 해당 코드를 시각적으로 표현한 것이다.
        • 여기서 확인할 것은, 입력값을 바꿈에 따라 label은 도시명으로 업데이트 된다는 것이다. 하지만 당장은 더미데이터만을 업데이트 할 것이다. 여기서는 더미데이터가 잘 표시되는 것만 확인하면 된다.
      • 참고: catchErrorJustReturn 연산자에 대해서는 추후에 배우게 될 것이다. 간단히 설명하면, 이 연산자는 API에서 받은 에러를 통해 observable이 dispose 되는 것을 막는 역할을 한다. 예를 들어, 존재하지 않는 도시명이 입력되었다면, NSURLSession에 404 에러가 리턴될 것이다. 하지만 여기선 이런 에러를 받아도 작업을 멈추지 말고 빈 값을 방출하도록 조치한 것이다.

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

  • 실시간 날씨 데이터를 API에서 받기 위해서는 인터넷 연결이 필요하다. API는 다음과 같은 JSON 형태의 response를 보낼 것이다.

     {
     	"weather": [
     		{
     			"id": 741,
     			"main": "Fog",
     			"description": "fog",
     			"icon": "50d"
     		}
     	],
     }
    • 상기 데이터는 현재 날씨와 관련이 있다. icon은 현재 상태에 맞는 아이콘을 표시하는데 사용될 것이다. 하단의 데이터들은 온도와 습도를 표시하는데 사용될 것이다.

       	"main": {
       		"temp": 271.55,
       		"pressure": 1043,
       		"humidity": 96,
       		"temp_min": 268.15,
       		"temp_max": 273.15
       	}
       }
      • 놀라지마세요. 저 온도는 섭씨나 화씨가 아닌, 캘빈 온도(K)입니다.
  • ApiController.swift에는 iconNameToChar라는 함수가 있다. 이는 String(JSON 내의 icon 값)을 받아서 UTF-8 코드의 String을 반환한다. 반환된 String은 현재 날씨에 대해 시각적인 날씨 아이콘을 보여주는 역할을 하게 될 것이다.

  • 같은 파일 내 buildRequest라는 함수도 있다. 이는 네트워크 리쿼스트를 생성하기 위한 것이다. 이 함수는 RxCocoa의 NSURLSession wrapper를 이요해서 네트워크 리퀘스트를 보낸다. 하기와 같은 작동을 한다.

    • 기본 URL을 가져오고, 구성요소를 추가하여 GET 또는 POST를 리퀘스트를 올바르게 작성한다.

    • application/json에 요청할 콘텐츠 타입을 설정한다.

    • metricsunits로 요청한다. (여기서는 캘빈 온도)

    • JSON 객체로 데이터를 매핑하여 반환한다. 이 부분에 해당하는 것이 이 함수의 return 부분이다. 하기 코드를 확인할 것.

       return session.rx.data(request: request).map { try JSON(data: $0) }
      • data 함수를 사용하는 NSURLSession을 중심으로 rx extension을 사용한다.

      • 이를 통해 Observabal<Data>를 반환하게 되고, 이 데이터는 raw 데이터를 JSON 유형의 SwiftyJSON 구조로 변환하는덷 사용되는 map 함수에 입력된다.

      • 이를 그림으로 나타내면 다음과 같다. 아래 그림은 ApiController 내부에서 어떤일이 일어나는지 시각적으로 나타낸 것이다.

  • 더미데이터를 실제데이터요청으로 바꾸는 것은 간단하다. Observable.just([...]) 호출을 실제 데이터 네트워크 요청으로 바꿔야 한다.

  • OpenWeatherMap API 문서(확인)에는 api.openweathermap.org/data/2.5/weather?q={city name}를 통해 도시명을 입력하면 현재 날씨를 확인할 수 있는 방법이 설명되어 있다.

  • ApiController.swift에서 임시 currentWeather(city:) 메소드를 다음 코드로 변경한다.

         func currentWeather(city: String) -> Observable<Weather> {
             return buildRequest(pathComponent: "weather", params: [("q", city)])
                 .map { json in
                     return Weather(
                         cityName: json["name"].string ?? "Unknown",
                         temperature: json["main"]["temp"].int ?? -1000,
                         humidity: json["main"]["humidity"].int ?? 0,
                         icon: iconNameToChar(icon: json["weather"][0]["icon"].string ?? "e")
                     )
             }
         }
    • 리퀘스트는 일부 대체 값을 사용하여, 사용자 인터페이스에서 예상하는 날씨 데이터 구조로 변환될 수 있는 JSON 객체를 반환한다.
  • 이제 London 같은 도시명을 입력해보자. 값이 잘 표시되는 것을 확인할 수 있다.

  • flatmap 내부의 catchErrorJustReturn 연산자를 제거하자. 제거하면 유효하지 않은 도시명을 입력하자마자 바로 404 에러를 받을 것이다. 앱은 작동을 멈출 것이다. 왜냐하면 observable이 에러를 방출하면서 dispose 되었기 때문이다.

    • 404 에러를 받은 것은 로그를 통해 확인할 수 있다. Failure (558ms): Status 404 Unhandled error happened: HTTP request failed with '404'

C. observable 바인딩하기

  • 바인딩에 대해선 다소 논란의 여지가 있다. 예를 들어 오랫동안 macOS의 중요한 부분이었음에도 불구하고, Apple은 iOS에서 바인딩 시스템인 Cocoa Binding을 출시하지 않았다.
  • Mac 바인딩은 macOS SDK에서 제공하는 매우 진보적인 클래스다.
  • RxCocoa는 프레임워크에 포함된 몇가지 유형에만 의존하는 간단한 솔루션을 제공한다. RxSwift 코드가 익숙해졌다면 아주 신속하게 바인딩을 사용할 수 있다.
  • 여기서 한가지 유의할 점은, RxCocoa에서의 바인딩은 단방향 데이터 스트림이라는 것이다. 이는 앱에서의 데이터 흐름을 크게 단순화 하는 방법이다. 이 책에서 양방향 바인딩에 대해선 다루지 않는다.
    • 참고: 만약에 데이터 모델 프로퍼티와 텍스트 필드 사이 같이, 양방향 바인딩을 경험해보고 싶다면 2개의 Producer와 2개의 Receiver가 필요하다. 이렇게 하면 코드 복잡도가 증가한다. 정말 해보고 싶으면 한번 해보렴.

1. observable 바인딩이란?

  • 바인딩을 이해할 수 있는 가장 쉬운 방법은, 두 개의 연결된 속성에 대한 관계를 생각해보는 것이다.

    • Producer는 값을 만들어낸다.
    • Receiver는 만들어진 값을 수신하고 처리한다.
    • Receiver는 값을 반환할 수 없다. 이 것이 RxSwift에서 사용하는 일반적인 바인딩 규칙이다.

bind(to:)

  • 바인딩을 할 수 있는 기본 함수는 bind(to:)다. observable을 다른 속성에 바인딩하기 위해서는 Receiver가 ObservableType 이어야한다.
  • 앞서 이러한 속성은 Subject라는 이름으로 설명된적 있다. Subject는 값을 만들어내고 수동적으로 쓰여질 수 있다. (Subject 다시보기)
  • Subject들은 Cocoa 환경에서 아주 중요한 역할을 한다. UILabel, UITextField, UIImageView같은 아주 기본적인 아이들을 생각해보자. 이들은 set 또는 get 할 수 있는 변경 가능한mutable 값들을 가지고 있다.
  • 요약하자면, bind(to:) 함수는 subscribe()의 특별 맞춤 버전이다. bind(to:)는 호출되었을 때 부수작용이 없다.

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

  • 이제 바인딩이 뭔지 알았으니, 이를 앱에 적용시켜보자.

  • 첫 번째로 할 일은, UILabelsubscribe(onNext:)에 적절한 데이터를 할당하기 위해 긴 observable을 리팩토링 하는 것이다.

  • ViewController.swiftviewDidLoad()를 아래의 코드로 변경하자.

     	// 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)
    • 주석을 따라 살펴보자.
        1. 이렇게 flatMapLatest 부분을 변경하므로써, 검색 결과는 재사용 될 수 있으며, 일회용 데이터 소스를 여러번 사용되는 Observable로 변형하게 된다.
        • 이렇게 변형함으로써 얻을 수 있는 장점은 추후 다룰 MVVM에서 다룰 것이다.

        • 다만 여기서 간단히 확인할 수 있는 것은 observable이 Rx의 재사용 가능한 속성이 될 수 있다는 것이다.

        • 올바른 모델링은 가독성이 낮은 일회성 코드들을 가독성이 높은 재사용 가능한 코드로 전환하는 것이다.

        • 이 작은 변화로, 아래와 같은 사용이 가능하다.

        1. 다른 구독의 모든 단일 파라미터를 처리하여 표시해야하는 값을 각각 매핑할 수 있다. 이는 온도를 나타내는 String을 반환하는 observable을 만들어낼 것이다. bindTo를 이용하여 오리지널 데이터소스와 temperature label을 바인딩 하자.
        1. 나머지 습도, 도시명, 아이콘에 대해서도 같은 작업을 진행할 수 있다.

D. Traits를 이용한 코드 개선

  • RxCocoa는 bindTo 외에도 Cocoa 프레임워크와 UIkit을 다룰 많은 진보된 기능들을 제공한다.
  • Trait은 UI와 함께 사용되도록 독점적으로 생성된 observable 항목의 특수한 구현을 제공한다. Trait는 직관적이고 작성하기 쉬운 코드를 작성하는데 도움이 되는 Observable의 특수 클래스다. (특히 UI 작업할 때)
  • 참고: RxSwift의 traits와 마찬가지로 RxCocoa의 traits도 작업에 도움이 되는 특별기능일 뿐 사용은 선택적이다.

1. ControlProperty와 Driver란?

  • 문서에는 Trait에 대해 다음과 같이 설명하고 있다. Traits는 observable sequence 객체가 인터페이스 영역과 소통할 수 있도록 도와준다. 개념적으로 어려울 수 있는데, 간단히 trait의 사용법을 정리해보면 다음과 같다.
    • Trait은 에러를 방출할 수 없다.
    • Trait은 메인 스케줄러에서 관찰한다.
    • Trait은 메인 스케줄러에서 구독한다.
    • Trait은 부수작용을 공유한다.
  • Trait 프레임워크의 주요소는 다음과 같다.
    • ControlProperty: 데이터와 유저인터페이스를 연결할 때 rx extension을 통해 사용한 적이 있다.
    • ControlEvent: 텍스트필드에서 글자를 입력할 때 리턴버튼을 누르는 것과 같이, UI구성요소에서의 확실한 이벤트를 듣기위해 사용한다. ControlEvent는 구성요소가 UIControlEvents를 현재 상태에 계속 두고 사용할 때 사용 가능하다.
    • Driver: 에러를 방출하지 않는 특별한 observable이다. 모든 과정은 UI 변경이 background 쓰레드에서 이뤄지는 것을 방지하기 위해 메인 쓰레드에서 이뤄진다.
  • Trait를 억지로 사용할 필요는 없다. 처음에는 순수히 Subject나 Observable만 쓰는 것도 나쁘지 않다. 하지만 만약 컴파일링 중에 또는 UI와 관련된 어떤 예정된 법칙을 체크하고 싶을 때, Trait은 아주 강력한 기능을 제공하며 시간 절약에도 좋다.
  • Trait을 사용하면 .observeOn(MainScheduler.instance) 호출에 대해 잊어버려도 좋다. 또한 background 쓰레드에서 UI를 생성할 필요도 없다.
  • DriverControlProperty가 지금은 어려워 보일 수 있다. 천천히 하나씩 확인해보자.

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

  • 모든 작업이 정확히 올바른 쓰레드에서 작동하고 있으며, 어떠한 에러도 발생하지 않아서 에러를 통한 구독 중지도 일어나지 않는 어플리케이션을 만들어봅시다.

  • 처음 할 일은 날씨 데이터 observable을 driver로 변형시키는 것이다.

  • viewDidLoad()내의 search를 선언한 부분을 하기의 코드로 바꾸자.

     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)
    • 여기서 중요한 것은 가장 마지막줄의 .asDriver(onErrorJustReturn: ApiController.Weather.empty)이다. 이 메소드를 통해 기존의 observable은 Driver로 전환된다.
    • onErrorJustReturn 파라미터는 observable이 에러를 방출할 때 어떻게 할 것인지 기본값을 정의하고 있다. 그러므로 driver는 스스로 방출된 에러를 떼어내는게 가능하다.
  • driver로 전환할 수 있는 또다른 메소드도 있다.

    • asDriver(onErrorDriveWith:): 수동적으로 에러를 관리할 수 있고, 이런 관리 목적에서만 새로운 sequence를 반환할 수 있다.
    • asDriver(onErrorRecover:): 또다른 Driver와 함께 사용할 수 있다. 현재 Driver가 에러를 받았을 때 복구할 목적으로 사용할 수 있다.
  • 상단의 코드까지 작성했을 때 이상한 부분이 하나 있다. 왜냐하면 DriverbindTo 메소드를 가지고 있지 않기 때문이다. 따라서 Driver에서 bindTo처럼 행동하는 비슷한 놈을 찾아야 한다. 아래의 코드를 확인해보자.

         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)
    • Driver.drivebindTo와 다른 이름이지만 앱의 UI행동을 맡는 비슷한 놈이다.
  • 여기까지도 훌륭하지만 개선가능한 부분은 여전히 남아있다. 앱은 너무 많은 리소스를 사용하고, API 리퀘스트를 너무 많이 만들어낸다. 왜냐하면 텍스트필드에 글자를 입력할 때마다 통신하기 때문이다. 이건 개선할 필요가 있다. 따라서 상기의 let search = searchCityName.rx.text 부분을 다음과 같이 변경하자.

     // 1
     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)
      1. 이렇게하면 입력값은 반드시 유효하다고 생각할 수 있다. 따라서 기존의 빈칸이면 걸러내는 부분은 지울 수 있다.
      1. 이제는 유저가 "Search" 버튼을 탭했을 때만 날씨 정보를 받는다. 불필요한 네트워크 요청을 제거한 것이다. 또한 currentWeather(city:)를 통해 반환된 observable에 대한 catchErrorJustReturn() 호출을 삭제할 수 있다.

  • 기존의 스키마에서는 전체 UI를 업데이트한 하나의 observable을 사용했다. 여러개의 블럭을 통해 subscribe에서 bindTo로 전환하고 뷰컨트롤러에서 같은 observer를 재사용했다. 이러한 접근법은 코드를 재사용 가능하고 작업하기 쉽도록 만들어준다.
    • 예를 들어, 만약 현재의 기압을 유저인터페이스에 추가하고 싶을 때 해야할일은 스트럭쳐에 해당 객체를 추가하고, JSON 값을 매핑한 후 또 다른 UILabel에 해당 객체를 매핑하는 것이다. 아주 간단하다.

E. RxCocoa와 dispose 하기

  • 이번 예제 초반에 메인뷰컨트롤러에 bag이 있었다. 이는 뷰컨트롤러가 릴리즈 되었을 때 모든 구독의 dispose를 관리하기 위한 것이였다. 그런데 왜 weakunowned 키워드를 클로저 내부에서 사용하지 않는걸까?
    • 왜냐하면 이 앱은 단일 뷰컨트롤러이고 메인뷰컨트롤러는 앱이 구동되는 동안은 항상 스크린에 띄워져 있기 때문이다. 따라서 메모리 낭비를 여기서는 걱정할 필요가 없다.

RxCocoa에서의 unowned vs weak

  • RxCocoa와 RxSwift를 다룰 때 unownedweak에 대한 개념은 어렵게 다가올 수 있다.
  • 지금까지는 클로저가 추후에 이미 릴리즈된 self 객체를 부를 때를 대비해서 weak 키워드를 썼다. 이 때문에 self는 옵셔널이 되었다. unowned는 이런 옵셔널 self를 회피하고 싶을 때 사용했다. 하지만 unowned를 쓸 때는 해당 클로저가 호출되기 전에는 절대 해당 객체가 릴리즈 되지 않는다는 것을 보장해야한다. 그렇지 않으면 crash가 날 것이기 때문이다.
  • RxSwift, 특히 RxCocoa에서는 이 부분에 대한 좋은 가이드라인이 있다.
    • nothing: 절대 릴리즈 되지 않는 싱글톤 내부 또는 뷰컨트롤러 (root view controller 같은)
    • unowned: 클로저 작업이 실행된 후 릴리즈되는 모든 뷰컨트롤러 내부
    • weak: 상기 두개 상황을 제외한 케이스
  • 이 규칙들은 고전적인 EXC_BAD_ACCESS 에러의 발생을 방지해준다. 만약 이 규칙을 항상 준수한다면, 메모리관리에 별다른 문제가 없을 것이다.
  • 다만 raywenderlich.com의 Swift 가이드라인에서는 unowned를 절대 쓰지 말 것을 권고하고 있다.

F. 이제 어디로 가죠?

  • 지금까지 본 것은 RxCocoa라는 거대한 프레임워크 중 일부에 불과하다. 다음 장에서는 예제로 만든 앱을 좀 더 개선할 수 있는 방법에 대해 확인해볼 것이다.
  • 다만 그전에 .rx extension과 RxCocoa에 대해 좀 더 둘러보자. 사실 RxCocoa는 32개의 extension을 가지고 있다. 몇가지 예제를 살펴보자.

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가 값이 제공될 때마다 업데이트 되는 형식으로 나타날 것이다.
    • 또한 사용자는 업로드 과정을 눈으로 확인할 수 있는 지표를 확인할 수 있을 것이다.

G. Challenges

섭씨(℃)를 화씨(℉)로 바꾸기

  • 여기서의 도전과제는 섭씨를 화씨로 바꾸는 것이다. 해결방법은 여러가지가 될 수 있다
    • API 리퀘스트를 미터법Metric에서 야드파운드법Imperial로 바꾸자.
    • 섭씨값을 수학적인 방법을 통해 화씨로 매핑할 수 있다. temperature * 1.8 + 32
  • 기술적으로, 상기 해결법들은 각각의 장애물을 가지고 있다.
    • 첫번째 방법은 ApiController.swiftSubject를 변경하여야 한다.
    • 두번째는 짧고 간단할 수 있다. 이는 search observable을 UISwitch의 control 객체와 합치는 방식으로 해결할 수 있다.

A. 첫 번째 방법

 let unitsQueryItem = URLQueryItem(name: "units", value: "imperial")

B. 두 번째 방법


Artwork/images/designs: from RxSwift: Reactive Programming in Swift book, available at http://www.raywenderlich.com