Contents
Chào bạn đến với Fx Studio. Bài viết lần này sẽ không có khái niệm mới được truyền tải. Chúng ta sẽ tập trung giải quyết các yêu cầu của project. Và với yêu cầu chính sẽ là Merge Observables Input trong một ViewController.
Vì đây là phần tiếp nối các phần trước trong series RxSwift với phần 3 – RxCocoa. Nên bạn cũng cần phải nắm được kiến thức trước đó. Hãy bắt đầu với RxCocoa Basic – Display Data.
Nếu mọi thứ đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
-
- Xcode 12
- Swift 5.3
- RxSwift 5.0
Trong bài này, chúng ta sẽ sử dụng tiếp nối Project từ bài trước (Delegate Proxy), nên cũng khá quan trọng. Nếu bạn đã tự tạo một project khác, thì hãy tiếp tục với logic của bạn. Và tất nhiên, bạn có thể checkout lại project để có thể theo dõi bài viết kĩ hơn.
-
- Link: checkout
- Thư mục:
/Examples/BasicRxSwift
1. Vấn đề
Trong các phần trước, chúng ta đã lần lượt đi qua từng vấn đề một trong UIKit, chúng đã được giải quyết bằng RxCocoa. Và cũng từ đó chúng ta thấy có nhiều vấn đề mới phát sinh thêm.
Trong số đó, nguy hiểm nhất vẫn là các sự kiện khác nhau xảy ra, nhưng lại kích hoạt cùng 1 xử lý/tương tác trong project.
Ví dụ, lấy phần gọi API để phân tích.
- UITextField
- Khi gõ chữ thì chúng ta tiến hành kiểm tra TextField.
- Nếu kiểm tra xong cho text của TextField đã ổn rồi, thì sẽ gọi API
- User Location
- Lắng nghe sự update dữ liệu Current Location
- Mỗi khi dữ liệu nhận được mà phù hợp các tiêu chí search, thì tiến hành gọi API
- Data default
- Thường khi khởi tạo 1 ViewController, ta sẽ cung cấp dữ liệu mặc định cho nó.
- Trường hợp này sẽ được khởi tạo và gọi API
- … (còn nhiều trường hợp khác mà tuỳ thuộc vào yêu cầu của project)
Không chỉ đơn giản trong ví dụ đó là chỉ có 1 xử lý/tương tác được thực hiện. Mà chúng ta còn có các Side Effect khác hoặc các xử lý phụ đi kèm. Ví dụ như: loading view … cũng gây ảnh hưởng tới nhiều thành phần trên giao diện.
Và đó là những vấn đề thường ngày khi bạn làm dự án. Nhất là với các dự án thường xuyên thay đổi yêu cầu. Giải quyết nó thì không khó. Khó là …
Cần xử lý một cách tập trung và thống nhất.
Tạm thời, mình sẽ mượn lại ví dụ đang dang dở ở trên, để tiến hành viết tiếp câu chuyện này ….
2. Update Model API
Về model cho API thì bạn hãy mở file WeatherAPI.swift để xem. Chúng ta đã có function tìm kiếm thời tiết của một thành phố, với tham số là tên thành phố. Bây giờ, chúng ta sẽ thêm một function nữa.
func currentWeather(at coordinate: CLLocationCoordinate2D) -> Observable<Weather> { return request(pathComponent: "weather", params: [("lat", "\(coordinate.latitude)"), ("lon", "\(coordinate.longitude)")]) .map { data in let decoder = JSONDecoder() return try decoder.decode(Weather.self, from: data) } }
Function này về mặt ý nghĩa thì cũng tương tự. Nhưng khác nhau tham số truyền vào. Tham số lần này là toạ độ của một vị trí trên bản đồ. Và ở bên trong function, chỉ là thay đổi lại params
truyền vào thôi. Mọi thứ khác không cần thay đổi.
3. Create Location Search
Tại bài viết Delegate Proxy, ta dùng button.rx.tap
để bắt sự kiện người dùng. Từ đó kích hoạt đối tượng CLLocationManager
tiến hành tracking GPS.
locationButton.rx.tap .subscribe(onNext: { [weak self] in guard let self = self else { return } self.locationManager.requestWhenInUseAuthorization() self.locationManager.startUpdatingLocation() }) .disposed(by: bag)
Và khi nhấn Button, chương trình sẽ kiểm tra việc cấp phát quyền hay chưa. Nếu quyền truy cập tới GPS đã được cấp phát, thì sẽ bắt đầu việc tracking. Công việc tiếp theo là lắng nghe sự thay đổi dữ liệu Current Location, từ việc Custom Delegate Proxy của CLLocationManager.
locationManager.rx.didUpdateLocation .subscribe(onNext: { locations in print(locations) }) .disposed(by: bag)
Đã xong phần hồi tưởng lại quá khứ. Bây giờ, chúng ta tập trung vào công việc chính. Đó là:
Tạo Observables Input cho Current Location.
3.1. Current Location
Tạo đối tượng lắng nghe sự thay đổi dữ liệu của Current Location. Bạn mở file WeatherCityViewController, và tại function viewDidLoad thì thêm đoạn code sau vào.
let currentLocation = locationManager.rx.didUpdateLocation .map { locations in locations[0] } .filter { location in return location.horizontalAccuracy < kCLLocationAccuracyHundredMeters }
Tại bước này, chỉ là lấy Observable từ Delegate Proxy mà thôi. Tất nhiên, thêm chút điều kiện để lọc bớt các location
quá gần nhau.
3.2. Location Input
Tiếp tục, tạo đối tượng lấy sự kiện từ Button liên quan tới Location. Bạn thêm đoạn code này vào sau đoạn code trước.
let locationInput = locationButton.rx.tap.asObservable() .do(onNext: { self.locationManager.requestWhenInUseAuthorization() self.locationManager.startUpdatingLocation() })
Cứ khi nào người dùng kích vào Button. Thì chúng ta sẽ kích hoạt việc update Location từ đối tượng CLLocationManager. Quan trọng hơn nữa là bạn sẽ dùng tới nó để kích hoạt Loading View.
Lúc có dữ liệu mới của GPS update, thì đối tượng currentLocation
sẽ phát đi dữ liệu. Do đó, ta cần bắt được dữ liệu này.
let locationObs = locationInput .flatMap { return currentLocation.take(1) }
Và bạn chỉ cần lấy 1 phần tử mà thôi. Nhưng thay vì lấy dữ liệu, thì ta dùng flatMap
để biến nó thành 1 Observable. Việc này để tiện sử dụng cho nhiều việc sau.
Tóm lại:
- Nhấn button sẽ kích hoạt việc tracking Location
- Tạo đối tượng lắng nghe sự thay đổi Current Location
- Từ đối tượng của
button.tap
. Ta tiến hànhflatMap
để tạo ra các Observable, bằng cách lấy 1 dữ liệu từ đối tượng lắng nghe Current location.
Chúng ta sẽ sử dụng các Observable được tạo ra đó để request tới API.
4. Merge Search Inputs
Chúng ta đã có nhiều nguồn hay nhiều xuất phát điểm để tiến hành gọi API để lấy dữ liệu. Mọi thứ đối với bạn và bạn vẫn có thể handle trong tầm tay. Nhưng bạn có dám chắc một điều rằng: “mình bỏ sót 1 hay vài trường hợp nào đó không?”
Bạn biết rằng, cứ mỗi sự kiện gọi API, bạn sẽ nhận được dữ liệu. Cấu trúc dữ liệu bạn nhận được sẽ không đổi. Nên tại sao, chúng ta không hợp nhất tất cả sự kiện gọi API về 1 điểm duy nhất. Hay một nguồn phát duy nhất. Khi đó, công việc còn lại khá là đơn giản. Lắng nghe những gì nó phát ra.
Okay! tiến hành thôi.
4.1. Create Observables Input
Chúng ta sẽ làm gọn lại các Observables liên quan tới TextField và chúng cũng áp dụng tương tự cho Location Input. Bạn tiếp tục với file WeatherCityViewController, tại viewDidLoad thì bạn xoá đi dòng lệnh sau:
let search = searchInput .flatMapLatest { text in return WeatherAPI.shared.currentWeather(city: text) .catchErrorJustReturn(Weather.empty) } .asDriver(onErrorJustReturn: Weather.empty)
Sau đó, chúng ta tiến hành tạo Observable cho search với TextField. Bạn thêm đoạn code sau vào:
let textSearch = searchInput.flatMap { text in return WeatherAPI.shared.currentWeather(city: text) .catchErrorJustReturn(.dummy) }
textSearch
phụ trách việc gọi lại function currentWeather(city:)
với dữ liệu text
từ TextField. Chúng sẽ trả về một Observable.
Áp dụng tương tự cho Location Input. Bạn lại thêm đoạn code sau vào:
let locationSearch = locationObs.flatMap { location in return WeatherAPI.shared.currentWeather(at: location.coordinate) .catchErrorJustReturn(.dummy) }
locationSearch
sẽ gọi hàm currentWeather(at:)
với dữ liệu là location
. Nó sẽ trả về cho chúng ta một Observable.
4.2. Merge Observables Input
let search = Observable .merge(locationSearch, textSearch) .asDriver(onErrorJustReturn: .dummy)
Bạn nhẹ nhàng thêm đoạn code trên vào tiếp. Trong đó,
- Sử dụng toán tử
merge
với tham số là 2 Observable tạo ở trên. Sau đó biến đổi chúng nó thành 1 Drive bằng toán tửasDriver
, để có thể đưa dữ liệu trực tiếp lên các UI Control của UIKit. - Với toán tử
merge
thì sẽ phát đi dữ liệu khi các Observable con phát đi dữ liệu. Bạn sẽ an tâm, từ bất cứ sự kiện nào gọi API đi nữa. Chúng ta chỉ cầnsubscribe
tới Observable merge kia thì sẽ có được dữ liệu.
Các công việc còn lại chỉ là subscribe
hoặc drive
mà thôi. Chúng không có gì thay đổi hết.
search.map { "\($0.temperature) °C" } .drive(tempLabel.rx.text) .disposed(by: bag) search.map { $0.cityName } .drive(cityNameLabel.rx.text) .disposed(by: bag) search.map { "\($0.humidity) %" } .drive(humidityLabel.rx.text) .disposed(by: bag) search.map { $0.icon } .drive(iconLabel.rx.text) .disposed(by: bag) search.map { $0.cityName } .drive(self.rx.title) .disposed(by: bag) search.map { $0.cityName } .drive(self.rx.title) .disposed(by: bag)
4.3. Update Loading View
Trong bài Working with multi UI Control, bạn đã thêm một Loading View. Đây là 1 UI Control chịu tác động từ nhiều Observables. Do đó, chúng ta cũng phải thay đổi nó một chút nữa. Mục đích chính là đồng bộ với nhiều nguồn Inputs vừa được tạo ra.
Bạn mở file WeatherCityViewController, bạn tới đoạn code của loading
và sửa lại như sau:
let loading = Observable.merge( searchInput.map { _ in true }, locationInput.map { _ in true }, // update with search at Location search.map { _ in false }.asObservable() ) .startWith(true) .asDriver(onErrorJustReturn: false)
Trong toán tử merge
, bạn thêm một Observable nữa. Đó là locationInput
. Vì nó cũng là một trong các điều kiệu để kích hoạt Loading View.
Bạn vẫn giữ nguyên các phần còn lại và tiến hành build project và cảm nhận kết quả.
Tạm kết
- Tách các sự kiện và biến đổi thành các Observables
- Tạo các Observables tưng ứng với các Inputs theo các sự kiện
- Merge các Observables Input
- Cập nhật giao diện khi có dữ kiệu nhận được từ Observable Merge
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!
5 comments
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)
let locationObs = locationInput
.flatMap { return currentLocation.take(1) }
bản thân thằng currentLocation là Observable rồi, nó luôn được cập nhật mới nhất, bác đâu cần take(1) làm gì, bác tưởng nó là Observable hả
Tuỳ thuộc vào ý đồ của bạn muốn làm gì thôi nhoé.
– locationInput –> đã là 1 Observable rồi nhưng vẫn lấy `take(1)` vì chỉ muốn lấy 1 giá trị thôi.
– Do mình kết hợp nhiều thứ lại ở phía dưới. Nếu cứ để tự động cập nhật thì nó sẽ kéo theo nhiều thứ chạy theo. ==> phiền phức lắm.
Bài viết phục vụ cho mục đích tò mò là chính. Bạn cứ thoả sức sáng tạo thêm.
À mà đôi lúc mình viết xong mà chừ lại quên mất ý đồ viết bài là gì nữa. Nên ngẫm ra thì nó cũng bất hợp lý lắm. 😀
chết comment của mình bị xóa kiểu của thằng Observable đi kèm, ý mình là currentLocation nó là Observable của cái CLLocation rồi chứ ko phải của 1 mảng CLLocation
Comment không xoá nhoé, do mình chống spam thôi. Nên phải có phê duyệt đã mới hiện lên.