RxSwift — Migrate Delegates to BEAUTIFUL Observables
Let’s say you have a MKMapView, or any object that has a corresponding delegate / protocol that you need to implement. In this case, let’s say we want to know when the region of the map changed.
You’re probably used to typing something like this:
class MyController : MKMapViewDelegate {
@IBOutlet weak var mapView : MKMapView!
override void viewDidLoad(){
super.viewDidLoad()
mapView.delegate = self
}
func mapView(mapView: MKMapView, regionDidChangeAnimated: Bool) {
doSomethingWith(mapView.centerCoordinate)
}
}
This is a normal implementation of MKMapViewDelegate where you’d like to be known of the events.
However, this style is very much at odds with RxSwift’s style of Observables. Why don’t we transform this delegate method into Observable<CLLocationCoordinate2D> so it seamlessly works with the rest of our RxSwift API.
Wouldn’t it be nice if we could just do this?
var mapView = MKMapView();
mapView.rx_regionDidChangeAnimated
.subscribeNext{ (animated: Bool) in
print("Map region changed, animated: (animated)")
}
Or even this?
var mapView = MKMapView();
mapView.rx_centerDidChange
.subscribeNext{ newCenterCoord in
// so do something with it already
}
You can use this pattern with almost any type of scenario that requires delegates.
1 — Create a Proxy Class
We need to create a custom proxy class to house our delegate methods. This proxy class is just a class that RxSwift can use to create an adapter between the delegate world and the RxSwift Observable world. If what I said just sounded scary, you can simply see that it only needs to cast an incoming object as the view and the appropriate delegate.
You’ll need to import RxSwift and RxCocoa and inherit DelegateProxy, MKMapViewDelegate, and DelegateProxyType.
import RxSwift
import RxCocoa
class RxMKMapViewDelegateProxy: DelegateProxy, MKMapViewDelegate, DelegateProxyType {
//We need a way to read the current delegate
static func currentDelegateFor(object: AnyObject) -> AnyObject? {
let mapView: MKMapView = object as! MKMapView
return mapView.delegate
}
//We need a way to set the current delegate
static func setCurrentDelegate(delegate: AnyObject?, toObject object: AnyObject) {
let mapView: MKMapView = object as! MKMapView
mapView.delegate = delegate as? MKMapViewDelegate
}
}
2 — Create Extension Methods for the Delegate Methods that you want
We need to create an extension for MKMapView so we can port our delegate implementations into Observable properties.
First we need to create a generic property called “rx_delegate” of the type DelegateProxy. This is the object gives you back the proxy class we created:
extension MKMapView {
public var rx_delegate: DelegateProxy {
return proxyForObject(RxMKMapViewDelegateProxy.self, self)
}
}
After that we can observe the methods by their selector. Please be familiar with the selector format. We’ll observe the selector and get a parameterized array of the object passed through as [AnyObject?].
Create another property on the extension called “rx_regionDidChangeAnimated: Observable<Bool>” after the first methods. Since we only need to know if it’s animated we just need the second parameter by referring to the selector. It’d be redundant to ask for the mapView since we’re attaching an extension to it already.
extension MKMapView {
public var rx_delegate: DelegateProxy {
return proxyForObject(RxMKMapViewDelegateProxy.self, self)
}
public var rx_regionDidChangeAnimated: Observable<Bool> {
return rx_delegate.observe("mapView:regionDidChangeAnimated:")
.map { params in
return params[1] as! Bool
}
}
}
Great so now you can do:
var mapView = MKMapView();
mapView. rx_regionDidChangeAnimated
.subscribeNext{ (animated: Bool) in
// so do something with it already
}
But we really want to get the coordinate of the region changed. Why don’t we just create an additional extension method that maps that value back to an Observable<CLLocationCoordinate2D>.
Here’s the full extension snippet:
extension MKMapView {
public var rx_delegate: DelegateProxy {
return proxyForObject(RxMKMapViewDelegateProxy.self, self)
}
public var rx_regionDidChangeAnimated: Observable<Bool> {
return rx_delegate.observe("mapView:regionDidChangeAnimated:")
.map { params in
return params[1] as! Bool
}
}
public var rx_centerDidChange: Observable<CLLocationCoordinate2D> {
return rx_regionDidChangeAnimated.map({ (animated) -> CLLocationCoordinate2D in
return self.centerCoordinate
})
}
}
3 — All Done! Now listen for those changes:
var mapView = MKMapView();
mapView.rx_centerDidChange
.subscribeNext{ newCenterCoord in
// so do something with it already
}
GitHub of the project is here: https://github.com/mbalex99/RxSwiftMapViewDelegateProxyExample/blob/master/README.md
Originally published at Eden Messenger Blog.