Contents
Chào bạn đến với Fx Studio. Bài viết này là bài viết đầu tiên cho miền đất mới trong thế giới ReactiveX. Đó là RxCocoa cho nền tảng Cocoa của iOS & MacOS. À, trong phạm vi của series thì chỉ tập trung ở iOS thôi. Và bài viết này sẽ nói về vấn đề đầu tiên cần được giải quyết, là Display Data lên giao diện.
Hiển nhiên, bạn cần phải nắm được cơ bản về Reactive Programming trong iOS với RxSwift. Thì mới tiếp tục được phần mới này. Và nếu như bạn chưa biết về chúng thì có thể tham khảo 2 phần dưới đây.
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
Phần mới này, chúng ta sẽ demo trên iOS Project. Bạn có thể tạo 1 project và tự do sáng tạo theo ý muốn riêng của bạn. Ngoài ra, bạn có thể checkout source code demo ở đây.
-
- Link: checkout
- Thư mục:
/Examples/BasicRxSwift
Vì project demo của mình có rất nhiều code ở các phần khác nhau. Nên có thể bạn sẽ gặp rắc rối khi tham khảo. Do đó, bạn cũng nên xây dưng project riêng của bạn ngay từ đầu.
Về màn hình cho phần này, bạn sẽ thấy file WeatherCityViewController.swift.
- Nó dùng để hiển thị 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.
- Dữ liệu chúng ta sẽ lấy từ API.
Cơ bản giao diện cho ViewController thì như hình trên.
1. Tổng quan RxCocoa
Đầu tiên, bạn hãy xem qua cấu trúc thư mục của RxCocoa trước. Bạn có thể vào phần Pod project
và kích để hiện ra phần RxCocoa. Nó khá là rất rất nhiều file. Hơi loạn một chút.
Vể RxCocoa, đây là phần ReactiveX cho framework Cocoa & CocoaTouch. Nó mang đầy đủ bản chất cơ bản của họ hàng nhà Rx và Reactive Programming. Chỉ sử dụng trên 2 nền tảng của Apple là iOS & MacOS. Với nó thì bạn sẽ làm được gì?
- Thao tác trực tiếp với UIKit (iOS).
- Cung cấp các tiện ích mở rộng cho UIKit (các extension).
- Hỗ trợ các class/struct đặc thù cho cả Cocoa & CocoaTouch.
- Tạo nên sự tương đồng trong các nền tảng Rx khác.
- Có thể kết hợp cùng RxSwift để tăng cường sức mạnh.
- Nhiều thứ hay mà bạn có thể tự làm thêm.
- …
Mình chỉ hiện thị ra chừng đó thôi và vẫn chứ còn nhiều thứ hay lắm. Bạn hãy thử kích vào 1 file, như là UITextField+Rx.swift xem thử có gì trong đó. Nó cũng không nhiều code 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 ý nghĩa của nó. Nó là thuộc tính text
sẽ được Rx hoá và chúng ta sẽ dùng được nó như một Observable hoặc hơn thế nữa … 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 đó!
2. Setup Data
2.1. Create files
Dữ liệu là thành phần không thể nào thiếu trong một chương trình. Nó xem là máu thịt của cả hệ thống. Có nó thì các chức năng mới hoạt động được. Và cũng như bao lần trước, công việc đầu tiên cần làm là define dữ liệu.
Để 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 khảo 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 - Biến đổi data thành đối tượng một cách nhanh chóng
struct Weather: Decodable { let cityName: String let temperature: Int let humidity: Int let icon: String let coordinate: CLLocationCoordinate2D }
- 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 }
2.2. Dummy Data
Chúng ta đã có 2 file Model chuẩn bị cho phần dữ liệu của ứng dụng. Để test thử cơ chế subscribe
trong RxSwift hoạt động như thế nào trong project của mình. Và với việc subscribe tới nhiều UI Control 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à Weather - 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.
Bạn cần chú ý tới việc dùng iconNameToChar
, thì bạn thêm function sau:
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" } }
- Dó dựa theo kết quả trả về từ API mình định sẽ sử dụng sau.
- Link mô tả mã của icon với hình thời thiết
- 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 iOS & MacOS.
Đã tạm ổn cho setup & dummy data của Model. Và tiếp theo, chúng ta tiến hành làm công việc chính của bài hôm nay.
3. Subscribe.
3.1. Setup
Đối tượng chúng ta sẽ thao tác là trên WeatherCityViewController. Đầu tiên, bạn hãy xem qua code ban đầu của nó như sau:
import UIKit import CoreLocation import MapKit class WeatherCityViewController: UIViewController { // MARK: - Outlets @IBOutlet private var searchCityName: UITextField! @IBOutlet private var tempLabel: UILabel! @IBOutlet private var humidityLabel: UILabel! @IBOutlet private var iconLabel: UILabel! @IBOutlet private var cityNameLabel: UILabel! @IBOutlet weak var containerView: UIStackView! @IBOutlet private var activityIndicator: UIActivityIndicatorView! @IBOutlet weak var locationButton: UIButton! @IBOutlet private var mapView: MKMapView! @IBOutlet private var mapButton: UIButton! // MARK: - Life cycle override func viewDidLoad() { super.viewDidLoad() configUI() } // MARK: - private methods private func configUI() { title = "Weather City" } }
ViewController rất đơn giản, với các UI Control cơ bản. Bạn có thể tự do sáng tạo. Mình có các UILabel để hiển thị thông tin thời tiết trên ViewController. Và cũng như bao bài viết RxSwift trước đây, để sử dụng được Rx trong ViewController này thì bạn cần import
2 thư viện Rx vào.
import RxCocoa import RxSwift
Cái không thể thiếu là túi rác quốc dân (DisposeBag). Nó sẽ giúp bạn giải quyết rác sinh (bộ nhớ) 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()
3.2. Subscribe
Tại function viewDidLoad, tiến hành subscribe nào.
override func viewDidLoad() { super.viewDidLoad() configUI() WeatherAPI.shared.currentWeather(city: "") .observeOn(MainScheduler.instance) .subscribe(onNext: { weather in print("City: \(weather.cityName)") }) .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.
Thử build project và xem có in ra được giá trị như chúng ta mong muốn hay không.
4. Model to UI Control
Về phương diện lập trình nói chung và lập trình iOS nói riêng. Kể cả bạn sử dụng Programming Paradigm nào đi chăng nữa. Hay bạn sử dụng bất cứ Design Patterns nào. Hoặc code một cách chân phương thuần tuý, hoặc dùng full thư viện tới tận răng … thì bản chất của lập trình điều xoay quanh việc:
Quản lý luồng dữ liệu & luồng sự kiện.
Trong phạm vi bài này, chúng ta sẽ quan tâm tới luồng dữ liệu. Chính là cách bạn đưa chúng hiển thị lên giao diện cho người dùng nhìn thấy được.
Ta có Model và đã có dữ liệu rồi. Cũng đã subscribe luôn rồi. Giờ là phần hiển thị data lên UI Control thôi. Chỉnh sửa tiếp đoạn code subscribe 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. 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ả đã ổn hay 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"
, do 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.
5. UI Control to Model
5.1. Subscription
Chúng ta sang chiều thứ 2, là từ giao diện tới Model.
Nhưng vẫn là chỉ là trong phạm vi luồng dữ liệu và chưa đụng gì tới luồng sự kiện.
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, 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
Tại sao lại là text
?
Vài bạn sẽ thắc mắc vì có sự kiện tác động thì mới có sự biến đổi dữ liệu. Nhưng chúng ta đang làm việc trong thế giới Rx. Việc sự kiện tác động lên UI Control sẽ làm thay đổi giá trị nào thuộc tính nào đó của nó. Tuy nhiên, với sự mở rộng thuộc tính sang không gian Rx thì nó trở thành nguồn phát (Observable). Hiển nhiên, khi có dữ liệu thì nó sẽ phát dữ liệu đi.
Chúng ta chỉ còn lắng nghe (subscription) từ chúng mà thôi.
Bạn có thể subscribe tới nó hoặc có thể thay đổi giá trị mới cho nó. Okay, chúng ta sẽ có 1 bài về em nó sau nha. Giờ sử dụng em nó nào. Tiếp tục với viewDidLoad của WeatherCityViewController, bạn thêm đoạn code sau vào:
searchCityName.rx.text.orEmpty .filter { !$0.isEmpty } .flatMap { text in return WeatherAPI.shared.currentWeather(city: text).catchErrorJustReturn(Weather.empty) }
Việc 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. Với ý nghĩa sau:
.orEmpty
để đảm bảo nếuemit
giá trị lànil
thì chúng sẽ biến thành""
.- 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 functioncurrentWeather
ở 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 và 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.
5.2. Update data
Tạm thời, ta về lại file Weather.swift và thêm 2 biến static
mới là empty
& dummy
, như sau:
static let empty = Weather( cityName: "Unknown", temperature: -1000, humidity: 0, icon: iconNameToChar(icon: "e"), coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0) ) static let dummy = Weather( cityName: "Fx Studio", temperature: 99, humidity: 99, icon: iconNameToChar(icon: "01d"), coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0) )
Và tiếp tục sửa lại function currentWeather
của file WeatherAPI.swift . 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"), coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0)) ) }
Quay lại file WeatherCityViewController, 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à subscribe nó. Chỉnh sửa 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)
Trong đó:
UITextField —>
.filter
—>flatMap
to Observable —>subscribe
Đó là sơ đồ mô tả cho công việc trên và nhằm giúp bạn có cái nhìn tổng quát hơn xí.
5.3. Test
Bạn hãy build ứng dụng và xem màn hình chúng ta sẽ hiển thì gì. Vì khi mới bắt đầu khởi chạy màn hình, lúc đó:
- UITextField chưa có dữ liệu gì hết
- Chúng ta gọi 1 lần tới function
currentWeather
với dữ liệu cho tham sốcity
là""
Nên kết quản hiển thị như hình dưới. Với Label City Name
không có dữ liệu.
Giờ, bạn hãy gõ vài chữ trong UITextField, ví dụ như là Đà Nẵng
. Bạn sẽ thấy Label City Name
sẽ hiển thị đúng theo nhưng gì bạn gõ, với 1 cách tức thời.
Nếu bạn test và ra được kết quả đúng như mong muốn, thì chúc mừng bạn đã hoàn thành được phần đầu tiên trong RxCocoa.
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.
Tạm kết
- Subscribe dữ liệu & hiển thị dữ liệu lên giao diện
- Cập nhật dữ liệu từ Model lên giao diện
- Biến đổi dữ liệu ở Model từ việc thay đổi giá trị Control trên giao diện.
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
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- 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)