07/09/2018, 17:10

RxSwift: Bài 6: RxCocoa (Part 1)

RxSwift: Bài 6: RxCocoa 1.Tổng quan Ta sẽ nghiên cứu 1 cái app lấy thời tiết ở 1 thành phố. Source code ở đây. Chúng ta sẽ dùng RxSwift, RxCocoa and SwiftyJSON để lấy data từ API. Sau này mình sẽ có 1 loạt bài viết về các cách lấy JSON trong đó có Codable. Các bạn lấy source về rồi vào ...

RxSwift: Bài 6: RxCocoa

1.Tổng quan
Ta sẽ nghiên cứu 1 cái app lấy thời tiết ở 1 thành phố. Source code ở đây. Chúng ta sẽ dùng RxSwift, RxCocoa and SwiftyJSON để lấy data từ API. Sau này mình sẽ có 1 loạt bài viết về các cách lấy JSON trong đó có Codable.

Các bạn lấy source về rồi vào phần RxCocoa như sau:
alt text

Rồi vào tiếp UITextField+RxRx:
alt text

Bạn sẽ thấy là xuất hiện kiểu ControlProperty , vậy nó là gì? Đây là một kiểu Subject mà có thể đc lắng nghe và có thể nhận được giá trị mới. Bạn tạm hiểu như vậy đã.

Bây giờ mở UILabel+Rx.swift lên, bạn sẽ thấy 1 type mới đó là Binder (trước đây phiên bản cũ nó là UIBindingObserver). Cái Observer này tương tự với ControlProperty, Nó được sử dụng chủ yếu để bind UI với logic cơ bản và quan trọng nhất, nó không bind được errors (bind ở đây giống như connect, lát mình sẽ nói về đoạn này sau).
alt text

Nếu có lỗi gửi đến Binder, nó sẽ gọi fatalError() trong khi đang run ở môi trường debug, nhưng sẽ được add và error log khi run ở production. Bây giờ ta đi vào cụ thể.

2. Displaying the data using RxCocoa
Ở đây, mình không đi sâu những phần ngoài rìa (như API,v..v) mà chỉ tập trung vào RxCocoa, mong các bạn thông cảm.
Trong ApiController.swift, bạn sẽ thấy 1 model dạng struct để map với JSON data. Sử dụng struct ở đây để cho code nhìn gọn hơn vì nó yêu cầu tất cả properties của nó phải có giá trị tại thời điểm khởi tạo. Trong trường hợp giá trị không hợp lệ, bạn luôn luôn có thể sử dụng "N/A" hoặc chuỗi tương tự:

struct Weather {
        let cityName: String
        let temperature: Int
        let humidity: Int
        let icon: String

        static let empty = Weather(
            cityName: "Unknown",
            temperature: -1000,
            humidity: 0,
            icon: iconNameToChar(icon: "e")
        )

Tiếp theo ta tạo 1 func mà dùng để lấy thời tiết hiện tại ở thành phố của mình, ta fake data 1 chút:

func currentWeather(city: String) -> Observable<Weather> {
  // Placeholder call
  return Observable.just(
    Weather(
      cityName: city,
      temperature: 20,
      humidity: 90,
      icon: iconNameToChar(icon: "01d"))
) }

Func này trả lại 1 fake data dùng để check tạm, sau này ta dùng data lấy từ server về để hiển thị cho nó. Mục đích là để làm việc với cấu trúc data mà không cần phải dùng server.

Đây là flow của ta, uni-directional data - dữ liệu đơn hướng:
alt text

Như đã nói ở các bài trước, RxSwift mà cụ thể là Observables có khả năng nhận và phát data hay đưa những giá trị này để xử lý tiếp. Cho nên nơi chính xác để lắng nghe 1 observable khi đang làm việc trong 1 VC là bên trong viewDidLoad().

Lý do bởi vì bạn cần subscribe càng sớm càng tốt, nhưng mà phải sau khi cái view được load lên. Nếu subscribing trễ 1 chút (ví dụ như viewWillAppear thì có thể sẽ miss 1 vài events hay 1 phần UI sẽ xuất hiện trước khi mình bind data cho chúng. Vì vậy bạn cần tạo tất cả các subscription trước khi app tạo UI hay request data mà cần xử lý để hiển thị.

Add dòng code sau vào 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
}

Build lên kết quả sẽ là:
alt text

Lúc này sẽ có 1 cái warning quen thuộc, subscribe is unused. Mình sẽ nhắc lại, 1 subscription returns 1 disposable object, cái mà sẽ cancel subscription khi cần. Trong trường hợp này, subscription phải được cancel khi VC bị dismiss. Add cho nó 1 cái bag phía cuối đoạn code

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
  })
  .dispose(by: bag)

Cái này sẽ cancel và dispose cái subscription ngay khi VC được released. Thứ nhất, điều này chống lãng phí resources, thứ hai chống những events ko mong muốn hoặc những ảnh hưởng khác có thể xảy ra khi 1 subscription không được disposed. Ví dụ nha, nếu subscription của bạn dùng để lắng nghe events từ server, nếu không dispose, nó cứ phát ra events và mình cứ query qoài, dễ bị memory leak.

3. rx property
Cái framework này sẽ dụng extensions của protocol và add thêm thành phần rx vào nhiều UIKit components. Bạn chỉ cần gõ rx. thì sẽ thấy các available properties và methods:
alt text

Có một property là text. Function này returns 1 observable là ControlProperty vừa thoả mãn ObservableType and ObserverType cho nên bạn có thể subscribe lẫn emit new value.

Add vào viewDidLoad():

searchCityName.rx.text
  .filter { ($0 ?? "").characters.count > 0 }
  .flatMap { text in
 return ApiController.shared.currentWeather(city: text ?? "Error")
    .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: bag)

Code ở trên sẽ trả về 1 Observable với data để hiển thị. Vì currentWeather không chấp nhận nil hay empty values nên tốt nhất filter chúng từ lúc nhập cho khoẻ.

Sau đó, fetch cái weather data bằng cách sử dụng lớp ApiController, đến đây mình không đi quá sâu vào phần lấy data từ server, chỉ yếu focus rx

Một khi bạn chuyển sang MainScheduler và main thread, bạn update tất cả control UI với dữ liệu hiện tại. Các bạn xem sơ đồ sau:

alt text

Lúc này, bạn gõ như thế nào label được update như thế đấy, có điều vẫn đang là fake data

Note: catchErrorJustReturn operator được yêu cầu để ngăn chặn việc Observable bị dispose khi mình nhận 1 error từ API. Ví dụ, 1 thành phố invalid name trả về 404 error NSURLSessionNSURLSession. Trong trường hợp nàynày, bạn nên trả về 1 empty value để app không bị dừng lại nếu có lỗi.

0