Contents
Chào bạn đến với Fx Studio. Bài viết lần này vẫn tiếp tục công việc Display Data trong thế giới RxCocoa. Nhưng lần này, dữ liệu chúng ta cần hiển thị sẽ là dữ liệu động và nó được lấy từ API.
Nếu bạn chưa đọc qua phần trước đó, thì tham khảo link sau:
Còn mọi thứ đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
-
- Xcode 12
- Swift 5.3
- RxSwift 5.0
Chúng ta vẫn dùng lại project của bài trước. Với một màn hình là WeatherCityViewController, để hiện thị thông tin thời tiết của một thành phố nào đó. Ngoài ra, bạn có thể checkout source code demo ở đây.
-
- Link: checkout
- Thư mục:
/Examples/BasicRxSwift
Bài lần này, chúng ta sẽ tập trung về mặt dữ kiệu và tương tác API nên phần giao diện sẽ vẫn giữ nguyên từ bài trước.
1. Weather API
Về API sử dụng cho bài viết này và các bài viết sau. Thì ta sử dụng API thời tiết của Open Weather Map và hiển nhiên là miễn phí rồi.
- Weather API : https://openweathermap.org/
Công việc của bạn bây giờ là đăng ký một tài khoản và lấy một key API
. Để sử dụng cho việc gọi các link API từ nó. Việc này khá EZ.
Vì tính bảo mật nên mình sẽ không để key API sẵn trong project demo.
Cũng có khá nhiều loại API được cung cấp trong hệ thống. Và cái mà ta sử dụng thì như sau:
- Current Weather : https://openweathermap.org/current
- Search by city name
Tất nhiên, các API trên là mình giới thiệu cho bạn. Bạn có thể dùng 1 API khác, thiết kế giao diện kiểu khác để phù hợp với dữ liệu của bạn nhận được.
2. JSON to Object
Công việc đầu tiên khi tương tác với API là bạn phải phân tích được dữ liệu bạn nhận được từ API. Theo như các API đề xuất ở trên, chúng ta có ví dụ cho dữ liệu trả về 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 }
Đó là dữ liệu đại diện cho thời tiết của một thành phố. Chúng ta hay so với class Weather thì thấy cấu trúc JSON phức tạp hơn. Khi dữ liệu cần lấy thì nó bị lồng vào trong vài cấp.
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ờ.
2.1. Define
Với class Weather thì đã kế thừa lại protocol Decodable, do đó bạn sẽ vẫn xử lý nó một cách gọn & đẹp được. Công việc mình sẽ tiến hành chỉnh sửa class Weather như sau:
CodingKeys
enum CodingKeys: String, CodingKey { case cityName = "name" case main case weather case coordinate = "coord" }
Đây là enum chính trong việc decode dữ liệu lên các thuộc tính của class Weather. Bạn sẽ thấy với cityName
và coordinate
thì trong cấu trúc JSON nó có tên khác. Đối với những trường hợp này, bạn có thể thay đổi giá trị của case
cho phù hợp.
Với main
& weather
thì sẽ phức tạp hơn một chút.
Main
enum MainKeys: String, CodingKey { case temp case humidity }
Trong cấu trúc JSON thì key main
sẽ chứa trong nó nhiều thứ. Khi bạn chỉ muốn lấy temp
& humidity
, thì bạn hãy khai báo thêm 2 case đó vào trong enum MainKeys. Áp dụng tương tự cho khi bạn muốn lấy thêm các giá trị khác. Lưu ý, chúng nó phải trùng tên với nhau.
AdditionalInfo
private struct AdditionalInfo: Decodable { let id: Int let main: String let description: String let icon: String }
Lần này, chúng ta sẽ đổi cách khác. Thay vì enum thì mình dùng struct. Bắt buộc phải kế thừa Decodable để thừa hường siêu năng lực biển đổi nhanh này. Tên của các thuộc tính trùng với key
trong cấu trúc JSON.
Coordinate
private struct Coordinate: Decodable { let lat: CLLocationDegrees let lon: CLLocationDegrees }
Enum này sẽ phục vụ việc lấy dữ liệu cho thuộc tính coordinate
. Vì chúng ta cần phải biến đổi một tí.
2.2. Decode
Sau khi đã hoàn thành việc define các enum & struct cho việc phân tích dữ liệu. Chúng ta sang phần Decode dữ liệu thành đối tượng. Với cấu trúc JSON trả về toàn chứa các thành phần bất hảo. Dó đó, chúng ta không thể nào áp dụng một cách tự động để có được Object ngay.
Bạn cần phải thêm hàm khởi tạo như sau:
init(from decoder: Decoder) throws { // Coding Keys let values = try decoder.container(keyedBy: CodingKeys.self) // CityName cityName = try values.decode(String.self, forKey: .cityName) let info = try values.decode([AdditionalInfo].self, forKey: .weather) // icon icon = iconNameToChar(icon: info.first?.icon ?? "") let mainInfo = try values.nestedContainer(keyedBy: MainKeys.self, forKey: .main) // temp temperature = Int(try mainInfo.decode(Double.self, forKey: .temp)) // humidity humidity = try mainInfo.decode(Int.self, forKey: .humidity) let coordinate = try values.decode(Coordinate.self, forKey: .coordinate) // coord self.coordinate = CLLocationCoordinate2D(latitude: coordinate.lat, longitude: coordinate.lon) }
Giải thích:
- Đầu tiên bạn cần phải
decode
bằng CodingKeys, nó là key chính. cityName
sẽ lấy được một cách đơn giản. Do nó đang là cấp đầu tiên trong cấu trúc JSON- Với thuộc tính
icon
thì bạn sẽ tạo ra một AdditionalInfo trước. Cũng bằng decode với keyweather
. - Với
temperature
&humidity
thì dùng kiểu CodingKeys lồng CodingKeys. Với key lồng là MainKeys. - Thuộc tính
coordinate
sẽ có được bằng việc khởi tạo 1 đối tượng CLLocationCoordinate2D. Nó không phải là dữ liệu có thể decode trực tiếp từ JSON được. Mà sẽ khởi tạo với 1 giá trịlat
&long
lấy từ việc decode.
Chúng ta đã thực hiện xong việc cài đặt biến đổi dữ liệu là JSON thành một Object với việc áp dụng Decodable. Cũng là khá EZ!
3. Connect API
Bây giờ, chúng ta sẽ hoàn thiện model WeatherAPI để phục vụ cho việc Display Data from API. Bạn ở file WeatherAPI.swift và tiến hành thêm các đoạn code như sau:
/// API key private let apiKey = "<your key api>" /// API base URL let baseURL = URL(string: "https://api.openweathermap.org/data/2.5")!
Đây chính là 2 thuộc tính cần sử dụng.
apiKey
chính là key mà bạn lấy được khi sử dụng các dịch vụ API của Open Weather Map.baseURL
là link cơ bản của một API. Các API khác nhau thì chúng ta sẽ tuỳ chỉnh các param thêm.
3.1. Request
Tiếp tục bằng việc thêm function sau cho WeatherAPI.
private func request(method: String = "GET", pathComponent: String, params: [(String, String)]) -> Observable<Data> { }
Mặc dù, mình đã viết một bài về tương tác API xịn sò với RxSwift rồi, nhưng mình vẫn muốn trình bày lại để cho bạn nhớ lâu hơn. Hoặc nếu bạn lười thì có thể sử dụng chúng.
Bước 1: URL & URLRequest
Chúng ta bắt đầu thêm code cho function request
. Tạo các đối tượng URL & URLRequest cho việc tương tác API.
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)!
Trong đó:
url
&request
là 2 đối tượng chính chúng ta sử dụng. Cũng khá cơ bản và không liên quan tới Rx- Với
NSURLComponents
giúp bạn thêm các query item cho link một cách nhanh nhất.
Bước 2: Add value
Với một API thì sẽ có nhiều Method, như: GET, POST, PUT, DELETE … Việc tiếp theo là xác định Method và truyền giá trị cần gởi lên API theo request vừa được tạo. Bạn tiếp tục thêm đoạn code sau.
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 }
Với method là:
- GET
- Dữ liệu sẽ được thêm vào
query string
(hay link URL). - Với dạng
key1=value1&key2=value2 ....
- Dữ liệu sẽ được thêm vào
- POST
- Dữ liệu sẽ đưa về dạng Dictionary (hay kiểu JSON)
- Thêm vào
httpBody
của request
Bổ sung thêm việc kiểm tra lần cuối url
của chúng ta có hoàn hảo hay không, thì bạn có thể sử dụng dòng code sau:
print("🔴 URL: \(urlComponents.url!.absoluteString)")
Tiếp tục là bạn sẽ hoàn thiện đối tượng request
với đoạn code sau.
request.url = urlComponents.url! request.httpMethod = method request.setValue("application/json", forHTTPHeaderField: "Content-Type")
Bước 3: Connect
Với 2 bước trên, chúng ta vẫn chưa dùng tới RxSwift hay RxCocoa. Giờ tới bước cuối, chúng ta sẽ sử dụng Rx trong việc gọi kết nối.
let session = URLSession.shared
Bạn sử dụng đối tượng singleton của URLSession và tiến hành gọi request thông qua lệnh sau:
return session.rx.data(request: request)
Kết quả của function này sẽ trả về cho ta một Observable với kiểu dữ liệu cho phần tử phát đi là Data. Và cũng kết thúc function request
này. Nó sẽ được gọi từ các function khác. Ví dụ như currentWeather
ở bài trước.
3.2. Parse
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) } }
Vì function request
trả về một Observable, thì chúng ta có thể subscribe
tới nó. Khi đó, kết nối sẽ được thực hiện và việc gọi API mới hoạt động. Còn về function currentWeather
chịu trách nhiệm gọi API và biến đổi data
thành đối tượng Weather.
Mọi thứ diễn ra một cách tự động theo những gì chúng ta đã cài đặt ở trên cho việc Decode JSON.
4. Update UI
Công việc cuối cùng chính là Display Data. Trước hết thì mình nhắc lại bản chất của Rx.
Một khi subscriber đăng ký tới một Observable, thì subscription sẽ được tạo. Và khi đó, Observable mới phát (emit) dữ liệu đi.
Áp dụng cho chương trình của chúng ta. Thì khi có subscribe
tới function currentWeather, lúc đó kết nối API mới được thực thi. Sau đó, bạn sẽ nhận được dữ liệu và tiến hành cập nhật lên giao diện.
Với cách code truyền thông trước đây. Chúng ta sẽ làm tới 3 công việc:
- Kết nối API
- Xử lý dữ liệu nhận được
- Cập nhật dữ liệu lên giao diện
Còn bây giờ với code Rx, công việc của chúng ta khá là đơn giản hơn nhiều.
- Subscribe tới Observable phụ trách kết nối
- Cập nhật lại giao diện
Tuy là 2 việc nhưng chúng sẽ liên kết với nhau, tạo thành một mạch (sequence) liên tục. Đó cũng chính là tư tưởng của Rx trong việc xử lý.
Nói dài như vậy thôi, còn về phần cập nhật giao diện này, bạn không cần phải làm gì hết. Chúng ta đã làm tất cả ở bài trước rồi. Công việc bây giờ chỉ còn là bấm nút build project và cảm nhận kết quả.
Bạn hãy gõ tên thành phố nào đó vào TextField, sao đó sẽ thấy kết quả hiển thị thông tin thời tiết của thành phố đó. Cuối cùng, để cho đẹp mắt. Bạn hãy xoá đi đoạn code này trong function viewDidLoad của file WeatherCityViewController.swift. Vì nó không còn ý nghĩa nữa rồi.
// First subscribe 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)
Tạm kết
- Define class/struct với Decodable, để biến đổi JSON thành Object một cách nhanh chóng
- Handle request API
Okay! Tới đây thì mình xin kết thúc bài viết này. Nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Phù thủy phiên dịch ý tưởng
- XML Delimiters – Mở khóa thế giới prompt phức tạp
- Instructions – Cung cấp hướng dẫn cho các Gen AI
- SMART – Hướng dẫn dành tạo Prompt cho người mới bắt đầu
- Nhìn lại năm 2024
- CO-STAR – Công thức vàng để viết Prompt hiệu quả cho LLM
- Prompt Engineering trong 10 phút
- Một số ví dụ sử dụng Prompt cơ bản khi làm việc với AI
- Prompt trong 10 phút
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
You may also like:
Archives
- January 2025 (5)
- December 2024 (4)
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)