Contents
Chào bạn đến với Fx Studio. Bài viết này là bài viết cuối cùng cho phần RxCocoa Basic trong đa vũ trụ RxSwift. Vì chúng ta đã trải qua nhiều phần lý thuyết riêng lẻ rồi. Và chúng ta sẽ tổng hợp lại trong bài viết này, công việc lần này sẽ được gọi là Extend UIKit.
Bạn cần phải nắm được tất cả kiến thức của các phần trước, để cho thấu hiểu phần này được nhiều hơn. Bạn yên tâm là những chỗ cần giải thích, thì mình sẽ để link tới các phần lý thuyết đó. Và nếu như mọi thứ ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
-
- Xcode 12
- Swift 5.3
- RxSwift 5.0
Project demo cho bài viết này sẽ sử dụng lại của bài viết trước đó. Với đối tượng chúng ta cần tiêu diệt là MKMapView. 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
Bạn hãy mở file WeatherCityViewController, thêm một MKMapView và nhớ kéo thả vào file *.xib. Sau đó, bạn hãy thêm sự kiện vào cho Button để quản lý việc ẩn hiện MapView trong ViewController. Bạn tham khảo đoạn code sau và hãy thêm nó vào hàm viewDidLoad của ViewController.
mapButton.rx.tap .subscribe(onNext: { self.mapView.isHidden.toggle() }) .disposed(by: bag)
Chuẩn bị như vậy thì ổn rồi. Quẩy tiếp nào!
1. Extend UIKit
1.1. Công việc Extend UIKit là gì?
Khi bắt đầu tìm hiểu về RxSwift, bạn sẽ bắt gặp rất nhiều từ khoá .rx
đã xuất hiện. Và chắc bạn cũng đã nghe mình nhắc tới nhiều từ khoá “Không gian Reactive“. Vâng, .rx
cũng là không gian Reactive.
Nó giúp phân biệt giữa không gian non-Rx, chính là code bình thường trước đây chúng ta hay dùng. Bên cạnh đó, tư tưởng của Rx nói chung cũng chính là mở rộng các class/struct của các nền tảng có sẵn. Và Cocoa & UIKit thì không thoát khỏi nó. Do đó, chúng ta cũng có rất nhiều methods & properties đã được Rx hoá. Chúng khá tương tự với các methods & properties non-Rx.
Ví dụ:
- UITextField có thuộc tính là
.text
. Đơn giản là một Store Property - UITextField được mở rộng
.rx
với thuộc tính là.rx.text
. Lúc này nó đóng vài trò vừa là nguồn phát (Observable), vừa là lưu trữ dữ liệu (tương tự như Store Property).
Hoặc trong không gian Reactive đó, những gì mà UIKit & Cocoa còn thiếu. Thì sẽ đc RxSwift bổ sung hết. Hoặc đơn giản là giúp cho người lập trình đơn giản hoá đi thao tác của mình.
Ví dụ: kinh điển chính là UIButton với .rx.tap
, là cách nhanh nhất mà bạn có thể đăng ký 1 sự kiện người dùng.
Vâng vâng và mây mây, tất cả chúng nó được gọi là
Extend UIKit
Tiếp theo, chúng ta sẽ đi qua từng phần trong việc ở rộng không gian Reactive của UIKit. Quẩy thôi!
1.2. Tạo các Extension
Công việc đầu tiên là bạn hãy chọn một class/struct mà bạn cần muốn mở rộng. Trong ví dụ của bài viết, class MKMapView sẽ là đối tượng cần tiêu diệt. Bạn hãy tạo một file mở rộng cho nó, đặt tên là MKMapView+Rx.swift.
Nhớ import đầy đủ các thư viện cần thiết vào trong file mở rộng.
Vì các class/struct trong UIKit & Cocoa thường sẽ không đi kèm Delegate Protocol của nó. Nên bạn cần phải khai báo thêm HasDelegate cho các struct/class đó.
extension MKMapView: HasDelegate { public typealias Delegate = MKMapViewDelegate }
Trong đó:
- Bình thường là MKMapView sẽ không có
delegate protocol
trong đó. Nên chúng ta sẽ kế thừa thêm protocolHasDelegate
- Sau đó khai báo thêm một
typealias
Delegate chính là protocolMKMapViewDelegate
Tiếp theo, bạn sẽ khai báo 1 class mới. Chúng nó được gọi là Proxy Class. Nhiệm vụ của nó là đấu mối trung gian giữa 2 thế giới Rx & non-Rx cho struct/class mà mình đang mở rộng.
class RxMKMapViewDelegateProxy: DelegateProxy<MKMapView, MKMapViewDelegate>, DelegateProxyType, MKMapViewDelegate { weak public private(set) var mapView: MKMapView? public init(mapView: ParentObject) { self.mapView = mapView super.init(parentObject: mapView, delegateProxy: RxMKMapViewDelegateProxy.self) } static func registerKnownImplementations() { self.register { RxMKMapViewDelegateProxy(mapView: $0) } } }
(Chi tiết việc giải thích về tạo các Extension này thì bạn hãy đọc thêm tại đây.)
1.3. Extension Reactive
Bây giờ, chúng ta sẽ tục mở rộng không gian Reactive cho MKMapView. Bạn thêm đoạn code sau vào:
public extension Reactive where Base: MKMapView { }
Mục đích chính là khi bạn thêm các thuộc tính và phương thức vào phần mở rộng trên. Thì bạn sẽ dùng với toán tử .rx
cho tụi nó. Tiếp tục, bạn thêm một property là delegate
. Đây chính là đối tượng Proxy Class ở trên. Và bạn chỉ cần dùng class tạo ở bước 1 và return nó về thôi.
var delegate: DelegateProxy<MKMapView, MKMapViewDelegate> {
return RxMKMapViewDelegateProxy.proxy(for: base)
}
Bây giờ, bạn đã có được không gian .rx
cho MKMapView và bạn có Proxy Class cho MKMapViewDelegate rồi. Công việc tiếp theo là bạn muốn gì thì hãy lấy cái đó.
2. Forward Delegate
Sau khi đã có được các Extension cần thiết rồi. Thì bạn tới phần tiếp theo là Forward Delegate.
(Phần này thì mình đã có một bài viết riêng cho nó tại đây.)
2.1. Tạo Forward Delegate
Bỏ qua phần giải thích dài dòng. Chúng ta sẽ tạo phương thức setDelegate
như sau:
func setDelegate(_ delegate: MKMapViewDelegate) -> Disposable { return RxMKMapViewDelegateProxy.installForwardDelegate(delegate, retainDelegate: false, onProxyForObject: self.base) }
Thêm đoạn code trên vào file MKMapView+Rx.swift. Với cách này chúng ta sẽ sử dụng như sau. Bạn về lại file ViewController. Tại function viewDidLoad
bạn thêm đoạn code sau:
mapView.rx.setDelegate(self) .disposed(by: bag)
Tại ViewController đó, bạn hãy implement các function cần thiết cho delegate của MKMapViewDelegate. Ta chọn function sau:
extension WeatherCityViewController: MKMapViewDelegate { func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { let pin = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "pin") pin.animatesDrop = true pin.pinTintColor = .red pin.canShowCallout = true return pin } }
Mình sẽ dùng hàm viewForAnnotation
của delegate MKMapView. Nó cần phải return cho nó 1 View. Chúng ta tạo 1 pin
cơ bản và return trở về.
2.2. Sử dụng
Vấn đề quan trọng tiếp theo, là sử dụng sao cho nó hài hoà với nhau. Bạn hãy nhớ lại Proxy của CLLocationManager. ta sẽ lợi chúng. Bạn tham khảo đoạn code sau:
locationManager.rx.didUpdateLocation .subscribe(onNext: { locations in for location in locations { let pin = MKPointAnnotation() pin.coordinate = location.coordinate pin.title = "Pin nè" self.mapView.addAnnotation(pin) } }) .disposed(by: bag)
Ta lắng nghe sự kiện phát ra từ việc didUpdateLocation
. Sau đó tạo 1 pin
và set vào MKMapView. Khi đó MKMapView sẽ yêu cầu delegate (chính là ViewController đã làm ở bước trên) thực thi function viewForAnnotation
.
Và chỉ có như vậy thôi. Bạn build lại ứng dụng và xem kết quả nhé.
3. Custom Binder
Đây là class để tạo ra các đối tượng mang tính chất là ObserverType. Nhằm để giúp việc bind
data của một Observable.
Trong không gian ReactiveX (Rx) của RxCocoa, thì ta có nhiều thuộc tính cho nhiều UI Control mang kiểu Binder này. Nó sẽ giúp liên kết chặt chẽ giữa property UI với dữ liệu của nguồn phát. Phản ứng lại với từng giá trị mà nó nhận được.
(Bạn có thể đọc thêm tại đây.)
Công việc Extend UIKit của chúng ta sẽ tiếp tục với việc phân tích thêm bài toán dưới đây.
3.1. Bài toán
Ở trên, chúng ta đang sử dụng dữ liệu thời tiết lấy được, để thêm một PIN vào bản đồ. Tuy nhiên, bài toán của chúng ta sẽ được nâng cao hơn như sau:
- Bạn đã có nhiều cách gọi API để lấy dữ liệu
- Mỗi khi có dữ liệu thì bạn sẽ thêm một PIN lên bản đồ
- Với thông tin thời tiết trong callout của PIN đó
Về việc lấy API, bạn đã theo dõi ở các bài trước. Chúng ta đã có được 1 Observable của API và lắng nghe được kết quả trả về. Nên công việc của bạn bây giờ chỉ đơn giản là biến đổi dữ liệu mà thôi.
Khi nghe đến biến đổi, bạn chỉ cần nhớ tới
map
huyền thoại của chúng ta.
3.2. Biến đổi
Bạn mở lại file WeatherViewController kia, và tại hàm viewDidLoad
bạn thêm đoạn code sau vào.
search.map { weather -> MKPointAnnotation in // coding here ... }
Trong đó
- Observable
search
, chúng ta đã tạo ra ở các bài trước. Bạn có thể xem lại nếu quên. map
dùng để biến đổi về mặt kiểu dữ liệu cho phần tử được phát đi từ Observable- Đưa kiểu Weather thành MKPointAnnotation. Đây chính là kiểu hiển thị PIN (tức MKPointAnnotationView ở phần trên)
Thêm ít code nha:
search.map { weather -> MKPointAnnotation in let pin = MKPointAnnotation() pin.title = weather.cityName pin.subtitle = "\(weather.temperature) °C - \(weather.humidity) % - \(weather.icon)" pin.coordinate = weather.coordinate return pin }
Phần này chắc mình không cần giải thích. Nó quá là EZ! Nhưng vẫn còn thiếu cái gì đó.
subscribe
3.3. New Binder
Dữ liệu đã có và nguồn phát đã sẵn sàng. Ở phần trên, chúng ta dùng subscribe
, nhưng nó xưa rồi. Chúng ta phải dùng trên RxCocoa và các Trait của nó. Nên việc tiếp theo là bạn tạo ra một Binder của riêng class MKMapView trong không gian Reactive. Bạn mở file MKMapView+Rx.swift thêm đoạn code sau vào Extension Reactive.
var pin: Binder<MKPointAnnotation> { return Binder(self.base) { mapView, pin in mapView.addAnnotation(pin) } }
Trong đó:
pin
là một thuộc tính trong không gian.rx
Binder
với kiểu làMKPointAnnotation
, nó sẽ nhận dữ liệu với chính kiểu kia. Lên đối tượngbase
là MapView- Công việc là dùng pin đó và thêm lên bản đồ thông qua hàm
addAnnotation(:)
Đơn giản phải không nào. Tiếp tục sang việc hoàn thiện nó nào!
3.4. Binding
Bạn về lại file WeatherViewController, tại đoạn code cuối cùng bạn thêm vào ở bước trên. Bạn tiếp tục thêm tiếp đoạn code sau:
search.map { weather -> MKPointAnnotation in let pin = MKPointAnnotation() pin.title = weather.cityName pin.subtitle = "\(weather.temperature) °C - \(weather.humidity) % - \(weather.icon)" pin.coordinate = weather.coordinate return pin } .drive(mapView.rx.pin) .disposed(by: bag)
Dùng drive
để sử dụng RxCocoa Trait (nó đã được biết đổi trên rồi), đưa trực tiếp lên pin
trong không gian rx
của MapView. Chúng nó tương thích với nhau, vì Binder là kiểu ObserverType
và Driver
hoạt động ở MainThread nên không gây ra crash chương trình khi cập nhật UI.
Cuối cùng, bạn đừng quên túi rác quốc dân nha. Xong rồi hãy build lại Project và tận hưởng kết quả nào.
Bạn sẽ thấy nhiều kết quả được tìm kiếm bằng API trên MapView. Kích vào mỗi PIN, là để xem thông tin thời tiết của thành phố đó.
Ngoài việc thêm Binder, bạn có thể thêm các sự kiện tuỳ chỉnh trong không gian Reactive. Với việc thay kiểu dữ liệu là Binder thành ControlEvent.
4. Proxy Delegate
Phần cuối cùng trong công việc Extend UIKit, đó là Proxy Delegate. Đây cũng chính là công việc quan trọng nhất & không thể thiếu được.
(Bạn có thể đọc thêm về Proxy Delegate tại đây.)
Về việc tạo các file & class cho Proxy Delegate, thì các bạn đã thực hiện ở trên rồi. Giờ chuyển sang bạn muốn lấy gì từ các Delegate của UI Control mà sẽ triển khai các bước tiếp theo.
Đối tượng ta vẫn là MKMapView & MKMapViewDelegate
Chúng ta lại tiếp tục nâng cao việc Extend UIKit bằng bài toán sau.
4.1. Bài toán
Chúng ta có một yêu cầu đơn giản là mỗi khi di chuyển MapView của chúng ta, thì…
- Bắt được sư kiện di chuyển đó
- Lấy được toạ độ của điểm chính giữa của bản đồ
- Tiến hành request API bằng với toạ độ kia
- Kết quả sẽ cập nhật lên bản đồ với 1 pin (được làm ở phần trên)
Bài toán của chúng ta khá là rõ ràng rồi. Chúng ta sẽ phân tích và triển khai nó trong không gian của .rx
. Với các bài trước & phần trên, thì chúng ta đã có đủ năng lực Rx hoá hết rồi. Chỉ còn lại mỗi việc bắt sự kiện di chuyển của MapView mà thôi.
Giải quyết chúng bằng việc cho Proxy Delegate bắt function của delegate và chuyển đổi thành Observable.
4.2. Method Invoked
Đối tượng cần xác định để uỷ thác là function sau trong MKMapViewDelegate.
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool)
Khi bạn cài đặt phương thức đó vào ViewController. Thì mỗi lần MapView bị di chuyển (tức drag
), thì function kia sẽ được gọi. Giờ chúng ta sẽ đưa nó vào không gian .rx
. Bạn hãy mở file MKMapView+Rx.swift
và thêm đoạn code sau vào.
var regionDidChangeAnimated: ControlEvent<Bool> {
let source = delegate
.methodInvoked(#selector(MKMapViewDelegate.mapView(_:regionDidChangeAnimated:)))
.map { parameters -> Bool in
return (parameters[1] as? Bool) ?? false
}
return ControlEvent(events: source)
}
Việc báo cho ViewController biết sự thay đổi trong MapView, thì nó cũng xem là một sự kiện từ MKMapView. Do đó, ta lựa chọn kiểu ControlEvent
cho thuộc tính được Proxy Delegate nhận uỷ thác từ MKMapViewDelegate.
Xác định function chính là regionDidChangeAnimated
để thực hiện việc invoked
. Tuy nhiên, nó có 2 tham số:
- Tham số
mapView
thì không cần lấy - Sẽ lấy tham số
animated
với kiểu là Bool.
Vì vậy, bạn chỉ cần thực hiện biến đổi với map
và return parameters[1]
về. Cuối cùng, bạn tạo ra 1 Trait ControlEvent
với event chính là source
.
4.3. React with Region changed
Bài cũng dài rồi và bạn nên nghỉ ngơi một chút để giải lao. Sau đó, bạn hãy tiếp tục Extend UIKit này.
Tư tưởng của RxSwift và Reactive Programming là lập trình phản ứng. Từ một sự kiện được phát ra. Chúng sẽ kéo theo một loạt các thay đổi và giao diện sẽ tự động thay đổi theo.
Về demo của chúng ta, tại file WeatherViewController thì là kết hợp nhiều nguồn phát ra sự kiện. Rồi từ đó sẽ gọi API và xử lý dữ liệu. Do đó, việc còn lại của chúng ta chen vào thêm 1 nguồn phát ra sự kiện cho Observable search
kia hoạt động.
(Bạn có thể đọc lại phần Merge Input API tại đây.)
Chúng ta bắt đầu tiếp nào!
4.3.1. Region Changed Input
Nó cũng giống như việc gõ chữ hay sự thay đổi về location của người dùng. Và đây là một sự kiện của MapView, chúng ta sẽ biến nó thành một Observable. Bạn thêm một đoạn code vào function viewDidLoad
của file WeatherViewController. Nhớ là trên các đoạn code tạo các search
cho các sự kiện.
let mapInput = mapView.rx.regionDidChangeAnimated
.map { [unowned self] _ in self.mapView.centerCoordinate }
Trong không gian .rx
, đã cài đặt thêm regionDidChangeAnimated
. Nó sẽ biến đổi sự kiện thành Observable. Bạn thêm một xí nữa là biến đổi kiểu dữ liệu. Chúng ta cần toạ độ chính giữa bản đồ của MapView. Nên dùng toán tử map
và return về centerCoordinate
.
Điều này được thực thi khi bạn
không
cần cósetDelegate
cho MapView là ViewController. Đó là ưu việt của Rx.
Tuy nhiên, chúng sẽ là các PIN mặc định. Muốn đẹp thì bạn phải cần có delegate
và custom lại các pin theo ý đồ của mình.
4.3.2. Search API with MapView
Bạn đã có sự kiện biến đổi thành Observable rồi. Tiếp theo, ta cần phải gọi API. Bạn thêm đoạn code sau vào:
let mapSearch = mapInput.flatMap { coordinate in return WeatherAPI.shared.currentWeather(at: coordinate) .catchErrorJustReturn(.dummy) }
Chúng ta dùng chính mapInput
ở trên. Tuy nhiên, lần này bạn phải biến đổi từ Observable này thành Observable khác. Do đó, toán tử cần sử dụng là flatMap
và để đảm bảo nó thực hiện bất đồng bộ.
4.3.3. Merge Search API
Kết thúc bước 2, chúng ta lại có thêm một Observable Search nữa. Công việc giờ đơn giản hơn. Chỉ bao gồm là merge
tất cả chúng nó lại với nhau. Bạn chỉnh sửa lại đoạn code tạo Observable search
.
let search = Observable .merge(locationSearch, textSearch, mapSearch) .asDriver(onErrorJustReturn: .dummy)
Khá là EZ, khi chỉ cần thêm mapSearch
vào cùng các anh chị em trước đó. Vẫn còn lại một cái nữa, đó là loading
. Bạn tiếp tục chỉnh sửa lại đoạn code của loading
như sau:
let loading = Observable.merge( searchInput.map { _ in true }, locationInput.map { _ in true }, // update with search at Location mapInput.map { _ in true }, // update with search at MapView search.map { _ in false }.asObservable() ) .startWith(true) .asDriver(onErrorJustReturn: false)
Chỉ cần thêm 1 dòng nữa là khi mapInput
hoạt động, thì sẽ phóng ra 1 giá trị là true
. Lúc đó Loading View sẽ hiện ra. Bạn hãy build lại project và cảm nhận kết quả tiếp nào.
Chỉ cần kéo rê MapView và khi dừng lại, thì ngay lập tức sẽ gọi API tìm kiếm thông tin thời tiết của điểm chính giữa bản đồ.
Tạm kết
Bài viết Extend UIKit cũng khá dài. Chúng ta đã đi qua các điểm lý thuyết sau:
- Tạo các Extension cho struct/class
- Mở rộng không gian Reactive cho struct/class
- Custom các methods & properties trong không gian Reactive với Binder
- Đăng ký các Delegate của UIKit với Forward Delegate
- Uỷ quyền các function của các Delegate với Proxy Class & Proxy Delegate
Okay! Tới đây thì mình xin kết thúc bài viết Extend UIKit và kết thúc chương RxCocoa Basic. 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
- 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
- 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
You may also like:
Archives
- December 2024 (3)
- 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)