Nhiệm vụ của View là chúng sẽ hiển thị dữ liệu cho người dùng xem và nhận sự kiện của người dùng. Thì việc chúng ta cần phải clear nó và nắm được 2 luồng cơ bản
- Luồng dữ liệu
- Luồng sự kiện
Đó là bản chất của lập trình.
Vẫn sử dụng tiếp project từ lúc đầu đến bây giờ. Vì đơn giản là mình lười mà thôi. Tất nhiên nó vẫn đủ sài cho mình và bạn. Nếu bạn đã quên nó ở đâu rồi thì có thể checkout tại đây:
- Link: checkout
- Thư mục:
/Examples/BasicRxSwift
Về màn hình cho phần này thì bạn sẽ thấy file WeatherCityViewController.swift
. Nó dùng để show thông tin thời tiết của một thành phố. Tên thành phố là do chúng ta nhập vào. Tất nhiên, dữ liệu chúng ta sẽ lấy từ API (mình sẽ tìm API thời tiết sau cho bạn).
Để xem qua bạn có thể vào phần Pod project
và show ra phần RxCocoa
. Nó khá là rất rất nhiều file. Hơi loạn một chút
Mình chỉ show ra chừng đó thôi, chứ còn nhiều lắm. Bạn thử vào 1 file như là UITextField+Rx.swift
xem thử có gì trong đó.
Cũng không nhiều lắm, tuy nhiên bạn sẽ thấy được vài thuộc tính điển hình như
/// Reactive wrapper for `text` property.
public var text: ControlProperty<String?> {
return value
}
Bạn cũng có thể đoán được, nó sẽ được Rx
hoá và chúng ta sẽ dùng được nó. ... vâng vâng và mây mây. Chúng ta sẽ tìm hiểu hết tất cả chúng trong các phần tiếp sau.
Đừng sa lầy vào đây nữa. Loạn rồi!
View có nhiệm vụ là hiển thị dữ liệu.
Vì chúng ta trong thể giới Rx
nên dữ liệu của chúng ta không đơn thuần là gán giá trị cho các thuộc tính của UI Control trên View. Dữ liệu sẽ được phát ra từ một nguồn phát (Observable) nào đó.
Để chuẩn bị dữ liệu cho Giao diện của chúng ta thì cần phải thêm các file model sau
Weather.swift
file này sẽ chứa class/struct với các thuộc tính tương đồng với dữ liệu dùng để hiển thị lên UI. Bạn tham sao code sau cho nó- Bạn nên sử dung protocol
Decoable
để nó có thể map trực tiếp dữ liệu JSON từ API và biết thành đối tượng một cách nhanh chóng
- Bạn nên sử dung protocol
struct Weather: Decodable {
let cityName: String
let temperature: Int
let humidity: Int
let icon: String
}
WeatherAPI.swift
Đây là file Model có nhiệm việc kết nối với API, phân tích dữ liệu và trả về cho nơi nào gọi nó. Bạn tham khảo code như sau- Vì class này sẽ có các đối tượng hay function liên quan tới RxSwift nên cần
import RxSwift
- Cũng vì lười nên mình 1 singleton đơn giản là 1 biến
static
thôi
- Vì class này sẽ có các đối tượng hay function liên quan tới RxSwift nên cần
import Foundation
import RxSwift
class WeatherAPI {
// MARK: - Singleton
static var shared = WeatherAPI()
// MARK: - Properties
// MARK: - init
init() { }
// MARK: - private methods
// MARK: - public methods
}
Chúng ta đã có 2 file Model chuẩn bị cho phần dữ liệu của ứng dụng. Giờ để test thử cơ chế subscribe
trong RxSwift hoạt động như thế nào trong project của mình với nhiều UI Controll cần dữ liệu từ nó. Chúng ta sẽ dùng dummy data
trước. Nếu mọi việc OKE thì sẽ tiến hành connect API để lấy dữ liệu sau.
Bạn mở file WeatherAPI.swift
và thêm function sau vào:
func currentWeather(city: String) -> Observable<Weather> {
return Observable<Weather>.just(
Weather(cityName: "Fx Studio",
temperature: 99,
humidity: 99,
icon: iconNameToChar(icon: "01d"))
)
}
Trong đó:
currentWeather
sẽ trả về dữ liệu cho têncity
được truyền vào- Function sẽ return về 1
Observable
với kiểu dữ liệu làWeahter
- Vẫn là toán tử huyền thoại
Observable.just
- Trong closure đó ta tạo mới 1 đối tượng Weather và gởi nó về thôi.
Chú ý chỗ iconNameToChar
thì bạn thêm function sau
- Dó dựa theo link này của API mình định sử dụng, link mô tả mã của icon với hình thời thiết
- http://openweathermap.org/weather-conditions
- Có điều kiện thì bạn hay tìm hình ảnh xịn sò hơn nha. Mình dùng tạm các emoji của MacOS
public func iconNameToChar(icon: String) -> String {
switch icon {
case "01d":
return "🌕"
case "01n":
return "🌕"
case "02d":
return "🌤"
case "02n":
return "🌤"
case "03d", "03n":
return "☁️"
case "04d", "04n":
return "☁️"
case "09d", "09n":
return "🌧"
case "10d", "10n":
return "🌦"
case "11d", "11n":
return "⛈"
case "13d", "13n":
return "❄️"
case "50d", "50n":
return "💨"
default:
return "E"
}
}
Tạm ổn cho setup và dummy data của Model. Giờ chúng ta lại sang file WeatherCityViewController.swift
. Cũng như phần làm việc với UIKit thì bạn cũng bắt đầu với import 2 thư viện RxSwift
& RxCocoa
Đầu tiên là túi rác quốc dân. Nó sẽ giúp bạn giải quyết rác sinh ra do quá trình hoạt động của ViewController. Bạn sẽ yên tâm về mặt bộ nhớ khi có mặt nó trong code của bạn.
let bag = DisposeBag()
Tại function viewDidLoad
, tiến hành subcribe
nào
override func viewDidLoad() {
super.viewDidLoad()
configUI()
WeatherAPI.shared.currentWeather(city: "")
.observeOn(MainScheduler.instance)
.subscribe(onNext: { weather in
// code here ...
})
.disposed(by: bag)
}
Quá là quen thuộc rồi. Mình sẽ lượt sơ lại cho bạn ôn bài cũ
WeatherAPI.shared
dùng đối tượng singleton đơn giản ở trên để gọi functioncurrentWeather
- Giá trị trả về là 1 Observable, nhưng ta hiểu là nó sẽ là dữ liệu từ việc connect API do đó cần phải
observeOn
tại MainThread - Cuối cùng là
disposed
vớibag
vừa tạo
Ta có Mode và đã có dữ liệu rồi. Cũng đã subcriber luôn rồi. Giờ là phần hiển thị data lên UI Control thôi. Edit tiếp đoạn code subcribe trên như sau:
WeatherAPI.shared.currentWeather(city: "")
.observeOn(MainScheduler.instance)
.subscribe(onNext: { weather in
self.cityNameLabel.text = weather.cityName
self.tempLabel.text = "\(weather.temperature) °C"
self.humidityLabel.text = "\(weather.humidity) %"
self.iconLabel.text = weather.icon
})
.disposed(by: bag)
Các đối tượng cityNameLabel
... là các IBOutlet của WeatherCityViewController
. Nếu dữ liệu nào là String thì gán trực tiếp. Còn dữ liệu nào Int thì cần biến đổi thêm xí.
Bạn hãy build và xem kết quả đã oke chưa. Bạn chú ý với tham số city
trong lời gọi hàm là ""
nhưng kết quả là "Fx Studio"
, thì đó là dummy data mà ta đã tạo trước rồi.
Tới đây, bạn đã hoàn thành được chiều đầu tiên của công việc này.
Đưa dữ liệu hiển thị lên UI Control
Tiếp tục với chiều ngược lại. Lần này dữ liệu sẽ là từ UI Control đưa về cho Model giải quyết. Trong bài demo này thì chúng ta sử dụng một UITextField
. Nó được dùng để nhập tên thành phố.
Bạn mở file UITextField+Rx.swift
trong không gian RxCocoa ở Pod thư mục. bạn sẽ thấy thuộc tính sau:
public var text: ControlProperty<String?> {
return value
}
Thuộc tính text
này là 1 ControlProperty
. Thực thể này khá là thú vị. Vì nó kết hợp
- ObservableType
- ObserverType
Bạn có thể subcribe tới nó và có thể thêm giá trị mới vào cho nó. Okay, chúng ta sẽ có 1 bài về em nó sau nha. Giờ sử dụng em nó nào.
searchCityName.rx.text.orEmpty
.filter { !$0.isEmpty }
.flatMap { text in
return WeatherAPI.shared.currentWeather(city: text).catchErrorJustReturn(Weather.empty)
}
Cũng tại function ViewDidLoad
của WeatherCityViewController
, bạn tiến hành sử dụng thuộc tính text
trên. Và muốn truy cập vào nó bạn hãy gõ .rx
trước. Đó là không gian của Reactive
có trong RxCocoa.
.orEmpty
để đảm bảo nếu emit
giá trị là nil
thì chúng sẽ biến thành ""
. Và dùng .filter
để lọc đi những giá trị là rỗng. Cuối cùng là flatMap
để biến đổi từ Text thành 1 Observable. Bằng việc gọi tới function currentWeather
ở trên.
Nếu trong quá trình làm việc (gọi API) mà lỗi thì sẽ nhận được 1 Error, tuy nhiên với catchErrorJustReturn
thì bắt phát chốt. Không cho nó thoát.
catchErrorJustReturn
sẽ xuất hiện khi có Error trong quát trình gọi API. Mà hiện tại chúng ta đang làm dummy toàn bộ. Nên khó mà thấy được.
Bạn cập nhập lại file Weather.swift
với 2 biến stactic
mới là empty
& dummy
static let empty = Weather(
cityName: "Unknown",
temperature: -1000,
humidity: 0,
icon: iconNameToChar(icon: "e")
)
static let dummy = Weather(
cityName: "Fx Studio",
temperature: 20,
humidity: 90,
icon: iconNameToChar(icon: "01d")
)
Và tiếp tục sửa lại function currentWeather
của file WeatherAPI
. Với việc sử dụng dữ liệu từ param
func currentWeather(city: String) -> Observable<Weather> {
return Observable<Weather>.just(
Weather(cityName: city,
temperature: 99,
humidity: 99,
icon: iconNameToChar(icon: "01d"))
)
}
Quay lại file WeatherCityViewController
thì ta đã lấy được sự kiện gõ text từ TextField và cũng biến đổi nó thành 1 Observable<Weather>
rồi. Việc tiếp theo là subcribe
nó. Edit lại nó như sau:
searchCityName.rx.text.orEmpty
.filter { !$0.isEmpty }
.flatMap { text in
return WeatherAPI.shared.currentWeather(city: text).catchErrorJustReturn(Weather.empty)
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { weather in
self.cityNameLabel.text = weather.cityName
self.tempLabel.text = "\(weather.temperature) °C"
self.humidityLabel.text = "\(weather.humidity) %"
self.iconLabel.text = weather.icon
})
.disposed(by: bag)
Tới đây, bạn hay build project và thử thay đổi giá trị của TextField. Xem thử cityNameLabel
của bạn có update theo không. Nếu đã update thì chúc mừng bạn đã thành công cho việc đưa dữ liệu từ UI Control về Model.
UITextField --->
.filter
--->flatMap
to Observable --->subscribe
Đó là sơ đồ mô tả cho công việc trên. Để giúp bạn có cái nhìn tổng quát hơn xí.
Hiển nhiên, dữ liệu dummy data từ Model không phải là cái mà chúng ta quan tâm. Nó chỉ giúp chúng ta kiểm tra xem việc cài đặt và thiết lập mọi thứ đã hoạt động nhịp nhàng hay chưa mà thôi. Vì chúng ta đã setup mọi thứ OKE rồi. Giờ sang công việc cuối cùng là lấy dữ liệu từ API về.
Thông tin về API
- Weather API : https://openweathermap.org/
Bạn đăng ký một tài khoản và lấy API Key
để sử dụng cho việc gọi các link API từ server. Chỉ cần vài nốt nhạc là oke thôi.
Link API chúng ta sử dụng là:
- Current Weather : https://openweathermap.org/current
- Search by city name
Hoặc bạn có thể tìm 1 API khác để sử dụng cho bài demo. Nhưng về bản chất thì sẽ giống nhau.
Tiến hành phân tích cấu trúc JSON từ API trả về. Ta có mẫu JSON như sau
{
coord: {
lon: -0.13,
lat: 51.51
},
weather: [
{
id: 801,
main: "Clouds",
description: "few clouds",
icon: "02n"
}
],
base: "stations",
main: {
temp: 10.74,
feels_like: 8.36,
temp_min: 10,
temp_max: 12,
pressure: 1020,
humidity: 81
},
visibility: 10000,
wind: {
speed: 2.6,
deg: 230
},
clouds: {
all: 15
},
dt: 1599364473,
sys: {
type: 1,
id: 1414,
country: "GB",
sunrise: 1599369706,
sunset: 1599417370
},
timezone: 3600,
id: 2643743,
name: "London",
cod: 200
}
Cấu trúc này khá là ói ăm khi name
chỉ có thể lấy trực tiếp. Còn 3 properties còn lại nó nằm trong cấu trúc main
. Mà chúng lại không giống tên của properties Weather
.
Ông trời không tuyệt đường sống của ai bao giờ. Bạn update Weather
lại như sau:
- Thêm 1 struct cho
main
private struct AdditionalInfo: Decodable {
let id: Int
let main: String
let description: String
let icon: String
}
- Thêm các
CodingKey
enum CodingKeys: String, CodingKey {
case cityName = "name"
case main
case weather
}
enum MainKeys: String, CodingKey {
case temp
case humidity
}
- Thêm function
init
để thực hiện việcdecode
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
cityName = try values.decode(String.self, forKey: .cityName)
let info = try values.decode([AdditionalInfo].self, forKey: .weather)
icon = iconNameToChar(icon: info.first?.icon ?? "")
let mainInfo = try values.nestedContainer(keyedBy: MainKeys.self, forKey: .main)
temperature = Int(try mainInfo.decode(Double.self, forKey: .temp))
humidity = try mainInfo.decode(Int.self, forKey: .humidity)
}
Việc decode
này không thuộc trọng tâm của RxSwift. Bạn hãy xem lại kiến thức đó ở link sau đây.
Tạm ổn cho phần cấu trúc dữ liệu nhoé
Bạn cần thêm các properties để khai báo các link & key API. Mở file WeatherAPI
và thêm vào
/// API key
private let apiKey = "<your key api>"
/// API base URL
let baseURL = URL(string: "https://api.openweathermap.org/data/2.5")!
Giờ thêm 1 function để request link API kia. Tiết kiệm thời gian thì bạn xem qua đoạn code sau:
private func request(method: String = "GET", pathComponent: String, params: [(String, String)]) -> Observable<Data> {
let url = baseURL.appendingPathComponent(pathComponent)
var request = URLRequest(url: url)
let keyQueryItem = URLQueryItem(name: "appid", value: apiKey)
let unitsQueryItem = URLQueryItem(name: "units", value: "metric")
let urlComponents = NSURLComponents(url: url, resolvingAgainstBaseURL: true)!
if method == "GET" {
var queryItems = params.map { URLQueryItem(name: $0.0, value: $0.1) }
queryItems.append(keyQueryItem)
queryItems.append(unitsQueryItem)
urlComponents.queryItems = queryItems
} else {
urlComponents.queryItems = [keyQueryItem, unitsQueryItem]
let jsonData = try! JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
request.httpBody = jsonData
}
print("🔴 URL: \(urlComponents.url!.absoluteString)")
request.url = urlComponents.url!
request.httpMethod = method
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let session = URLSession.shared
return session.rx.data(request: request)
}
Nó cũng tương tự phần UIKit với tạo Networking Model mà thôi. Bạn lỡ quên nó thì có thể quay lại đọc sau. Có một điều khác là chúng ta sẽ trả về data
chứ không phải là 1 đối tượng Observable<Weather>
return session.rx.data(request: request)
Phát cuối cùng, bạn về lại function currentWeather
và sửa lại như sau
func currentWeather(city: String) -> Observable<Weather> {
return request(pathComponent: "weather", params: [("q", city)])
.map { data in
let decoder = JSONDecoder()
return try decoder.decode(Weather.self, from: data)
}
}
Trong đó:
- gọi function
requesr
với các param theo tài liệu của API Document &city
từ người dùng nhập vào map
để biến đổi Data thành Weather, thông quaJSONDecoder
Khá là EZ phải không nào. Bạn hãy build project và kiểm tra lại chúng 1 lượt. Bạn nhập tên 1 thành phố và xem kết quả trả về như thế nào. Và lần này sẽ có error
cho bạn.
Cảm ơn bạn đã đọc bài viết này!
- Hiển thị dữ liệu từ Model lên UI Control
- Subscribe dữ liệu từ UI Control và tiến hành update
- Connect API và hiển thị dữ liệu lên lại UI Control