13.
Intermediate RxCocoa
Written by Shai Mishali
In the previous chapter, you were gently introduced to RxCocoa, the official RxSwift Cocoa extension framework. If you haven’t gone through that chapter, it would be a good idea to read through it so you’re ready to tackle this one.
In this chapter, you’ll learn about some advanced RxCocoa integrations and how to create custom wrappers around existing UIKit components.
Note: This chapter won’t discuss RxSwift architecture, nor will it cover the best way to structure a RxSwift/RxCocoa project. This will be covered in Chapter 23, “MVVM with RxSwift.”
Getting started
This chapter continues from the previous project.
Installing project dependencies
Open Terminal, navigate to the root of the project and run pod install to fetch the required dependencies. Once that’s completed, open Wundercast.xcworkspace to get started.
Getting an OpenWeatherMap API Key
To set up the project, you will need a valid OpenWeatherMap key. If you already have one, skip to the end of this section.
If you don’t have a key, you can create one at https://home.openweathermap.org/users/sign_up.
Once you’ve completed the signup process, visit the dedicated page for API keys at https://home.openweathermap.org/api_keys and generate a new key.
Open the file ApiController.swift and copy the newly generated key into the correct place:
private let apiKey = "Your Key"
Showing an activity while searching
The application currently displays the weather information of a given city, but the app gives no feedback once the user presses the Search button. It’s a good practice to display an activity indicator while the app is busy making network requests.
When you’re finished with this task, the app logic will look like this:
To achieve this, you’ll make a few changes to your current code to decompose the original observables into smaller ones, so that you’re notified when a user presses the button, and when the data has arrived from the server.
Open ViewController.swift. Go to viewDidLoad() and add the following code to the top of the function, below the call to style():
let searchInput = searchCityName.rx
.controlEvent(.editingDidEndOnExit)g
.map { self.searchCityName.text ?? "" }
.filter { !$0.isEmpty }
The searchInput observable will provide the text for a search when the input string is not empty and the user presses the Search button.
Now you can modify the search observable to use the searchInput observable instead of creating things from scratch. Modify search as follows:
let search = searchInput
.flatMapLatest { text in
ApiController.shared
.currentWeather(for: text)
.catchErrorJustReturn(.dummy)
}
.asDriver(onErrorJustReturn: .dummy)
Now you have two observables that indicate when the application is busy making requests to the API. One option is to bind both observables, correctly mapped, to the isAnimating property of UIActivityIndicatorView and do the same for all the labels with the isHidden property. This solution seems convenient enough, but in Rx there’s a far more elegant way to accomplish this.
The two observables searchInput and search can be merged into a single observable having the value of either true or false depending on whether or not they are receiving events. The result is an observable describing whether the application is currently requesting data from the server or not.
Below the code block you just added, append this:
let running = Observable.merge(
searchInput.map { _ in true },
search.map { _ in false }.asObservable()
)
.startWith(true)
.asDriver(onErrorJustReturn: false)
The combination of these two observables has this result:
The .asObservable() method on search is necessary, since it’s a Driver and your call to merge expects two Observables. .startWith(true) is an extremely convenient call to avoid having to manually hide all the labels at application start, it will immediately emit true as soon as a consumer subscribes to running.
At this point, the bindings will be very straightforward to create. You can place them before or after the bindings to the labels as it makes no difference which way you do it:
running
.skip(1)
.drive(activityIndicator.rx.isAnimating)
.disposed(by: bag)
You have to remember that the first value is injected manually, so you have to skip the first value, or else the activity indicator will display immediately once the application has been opened.
Next, add the following to hide and show the labels according to their status:
running
.drive(tempLabel.rx.isHidden)
.disposed(by: bag)
running
.drive(iconLabel.rx.isHidden)
.disposed(by: bag)
running
.drive(humidityLabel.rx.isHidden)
.disposed(by: bag)
running
.drive(cityNameLabel.rx.isHidden)
.disposed(by: bag)
After applying this change, the application should look like the following when it’s making an API request:
Here is what things should look like immediately after it opens. All labels should be hidden, but the activity indicator should not display:
Nice job! You can now add some new features to your app.
Extending CLLocationManager to get the current position
RxCocoa is not only about UI components; it comes with some convenient classes to wrap official Apple frameworks in a simple, customizable and powerful way.
A weather application that doesn’t know its current location is a bit odd, to say the least. You can fix this by using some of the components provided by RxCocoa.
Creating the extension
The first step to integrating the CoreLocation framework is to create the necessary wrapper around it. Open the file under Extensions named CLLocationManager+Rx.swift. This is the file where the extension will be created.
All the other extensions are behind the .rx namespace. For CLLocationManager, the goal is to follow the same pattern. This smart behavior is achieved by using the Reactive proxy provided by RxSwift.
Navigate to the RxSwift library inside the Pod project and find a file named Reactive.swift. Open the file and you’ll find a struct named Reactive<Base>, a ReactiveCompatible protocol and a default extension for ReactiveCompatible, which provides the rx namespace for any object which is ReactiveCompatible.
The last line is:
/// Extend NSObject with `rx` proxy.
extension NSObject: ReactiveCompatible { }
This is how every class inheriting from NSObject gets the rx namespace. Your job is to create the dedicated rx extensions for the class CLLocationManager and expose them for other classes to use.
Navigate into the RxCocoa folder inside the dedicated Pod project and you’ll find some Objective-C files named _RxDelegateProxy.h and _RxDelegateProxy.m as well as DelegateProxy.swift and DelegateProxyType.swift. These files contain the implementation of a rather clever solution to bridge RxSwift with any framework that uses delegates (and data sources) as the main resource for providing data.
The DelegateProxy object creates a fake delegate object, which will proxy all the data received into dedicated observables.
The combination of DelegateProxy and elegant use of Reactive will make your CLLocationManager extensions look just like all the other RxCocoa extensions already available. Neat!
CLLocationManager requires a delegate, and, for this reason, you need to create the necessary proxy to drive all the data from the necessary location manager delegate to the dedicated observables. The mapping is a simple one-to-one relationship, so a single protocol function will correspond to a single observable that returns the given data.
Navigate to CLLocationManager+Rx.swift and add the following code:
extension CLLocationManager: HasDelegate {}
class RxCLLocationManagerDelegateProxy: DelegateProxy<CLLocationManager, CLLocationManagerDelegate>, DelegateProxyType, CLLocationManagerDelegate {
}
RxCLLocationManagerDelegateProxy is going to be your proxy that attaches to the CLLocationManager instance right after an observable is created and has a subscription. This is simplified by the HasDelegate protocol, provided by RxCocoa. As expected, it also serves as the CLLocationManagerDelegate itself.
At this point, you need to add an initializer for the proxy delegate and a reference to it.
First, add the following init to the class:
weak public private(set) var locationManager: CLLocationManager?
public init(locationManager: ParentObject) {
self.locationManager = locationManager
super.init(parentObject: locationManager,
delegateProxy: RxCLLocationManagerDelegateProxy.self)
}
And then a method to register the proper implementations:
static func registerKnownImplementations() {
register { RxCLLocationManagerDelegateProxy(locationManager: $0) }
}
By using these two methods, you can initialize the delegate and register all implementations, which will be the proxy used to drive the data from the CLLocationManager instance to the connected observables. This is how you expand a class to use the delegate proxy pattern from RxCocoa.
Now, create the observables to observe the change of location, using the proxy delegate you just created. At the very bottom of the same file, add:
public extension Reactive where Base: CLLocationManager {
var delegate: DelegateProxy<CLLocationManager, CLLocationManagerDelegate> {
RxCLLocationManagerDelegateProxy.proxy(for: base)
}
}
Using the Reactive extension will expose the methods within that extension in the rx namespace for an instance of CLLocationManager. You now have an exposed rx namespace available for every CLLocationManager instance, but, unfortunately, you have no real observables to use.
Fix this by adding the following to the extension you just created:
var didUpdateLocations: Observable<[CLLocation]> {
delegate.methodInvoked(#selector(CLLocationManagerDelegate.locationManager(_:didUpdateLocations:)))
.map { parameters in
parameters[1] as! [CLLocation]
}
}
With this new Observable, the delegate used as the proxy will listen to all the calls of didUpdateLocations, getting the data and casting it to an array of CLLocations. methodInvoked(_:) is part of the Objective-C code present in RxCocoa and is a low-level observer for delegate invocation.
methodInvoked(_:) returns an observable that sends next events whenever the specified method is invoked. Each emitted element is an array of the parameters the method was invoked with. You access this array with parameters[1], accessing the second parameter — didUpdateLocations, and cast it to an array of CLLocation.
You are now ready to integrate this extension into the application.
Using the button to get the current position
Now that you’ve created the extension, you’ll be able to use the location button in the bottom left corner:
Switch to ViewController.swift to work on the app UI. Before proceeding with the button logic, there are a few things to take care of. First, import the CoreLocation framework at the top of the file:
import CoreLocation
Next, add a location manager property to your view controller:
private let locationManager = CLLocationManager()
Perfect! Your project is now ready to handle the location manager and retrieve the user’s location.
Note: Declaring a location manager instance inside
viewDidLoad()would cause a release of the object and the subsequent weird behavior of the alert being displayed and immediately removed oncerequestWhenInUseAuthorization()was called.
Now you need to make sure the application has sufficient rights to access the user’s location. In iOS, you must ask for the user’s permission before getting any location information in your app. Therefore, the first thing you need to do when the user taps the current position button is to ask for permission to use the current location data and then update the data.
To achieve this, add the following code inside viewDidLoad():
geoLocationButton.rx.tap
.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
})
.disposed(by: bag)
To test that the application is actually receiving the user’s location, add this temporary snippet:
locationManager.rx.didUpdateLocations
.subscribe(onNext: { locations in
print(locations)
})
.disposed(by: bag)
When you build and run the project, after tapping the locate button, you should see the output in the console similar to this:
Note: When using the simulator, you can fake the location under Debug ▸ Simulate Location and select one of the simulated locations.
At this point, assuming the user permitted the app to access their location, the app can use that location data to retrieve the local weather. There’s a dedicated method inside ApiController.swift to retrieve the data from the server based on the user’s latitude and longitude:
func currentWeather(at: CLLocationCoordinate2D) -> Observable<Weather>
This method will return a Weather instance from a set of coordinates.
Unifying authorization and location, reactively
You currently have a two-stepped mechanism to get the user’s location — you request authorization for their location, while simultaneously having a second subscription waiting for the locations to arrive.
Wouldn’t it be nice if we could compose these two together in a way that abstracts it for the consumer? Simply ask for a location, and get it — whether or not authorization was granted or needs to be asked for since that’s just an implementation detail.
This is a natural combination and where RxSwift’s compositional abilities really shine:
Switch back to CLLocationManager+Rx.swift. Before writing your reactive method to abstract the authorization and the location, you’ll need an observable that tells you whether or not the user has granted authorization.
Add the following property in your reactive extension, below didUpdateLocations:
var authorizationStatus: Observable<CLAuthorizationStatus> {
delegate.methodInvoked(#selector(CLLocationManagerDelegate.locationManager(_:didChangeAuthorization:)))
.map { parameters in
CLAuthorizationStatus(rawValue: parameters[1] as! Int32)!
}
.startWith(CLLocationManager.authorizationStatus())
}
This is relatively similar to the implementation of didUpdateLocations with two differences:
- The second parameter is a number, and not the concrete
CLAuthorizationStatustype, so you cast it appropriately and initialize a new instance ofCLAuthorizationStatuswith the raw value. - You use
startWithto make sure the consumer immediately gets the current status before any future changes are emitted.
Note: Using force unwrapping might seem ill-advised, but in the case of a delegate proxy, we are 100% positive the parameter will exist and result in a valid
CLAuthorizationStatus.
Nice — you now have an observable notifying you of the location authorization status.
Time to put everything together with the magic of composition!
While still inside the reactive extension, add the following method right below your new authorizationStatus property:
func getCurrentLocation() -> Observable<CLLocation> {
let location = authorizationStatus
.filter { $0 == .authorizedWhenInUse || $0 == .authorizedAlways } // 1
.flatMap { _ in self.didUpdateLocations.compactMap(\.first) } // 2
.take(1) // 3
return location // 4
}
Here’s what the code you added does:
- Subscribe to
authorizationStatusand wait for it to change to an authorized state. Remember that if the consumer already approved location services, thestartWithinauthorizationStatuswill take care of immediately notifying you upon subscription. - Once you have an authorized status, you use
flatMapto switch to thedidUpdateLocationsobservable you previously defined and get the first location of the emitted array of locations. - You only need a single location, so you use
take(1)to immediately complete once you get the first location. - Finally, you return the observable you just created to the consumer.
You now have a method that will wait for a proper authorization status, and immediately wait for the first available location and emit it back to the consumer.
But what’s missing? Well, we didn’t ask the location manager to do anything yet!
Add the following two lines immediately before the return statement:
base.requestWhenInUseAuthorization()
base.startUpdatingLocation()
In this case, base refers to the object you’re creating a reactive extension for — CLLocationManager. You use it to ask for a “When in Use” authorization and start getting location updates.
Finally, you want to be a good citizen and clean up after you’re done. Immediately after .take(1), append this final statement:
.do(onDispose: { [weak base] in base?.stopUpdatingLocation() })
Using the do operator, you instruct the location manager to stop getting location updates as soon as the subscription is disposed of, which would happen when you get the first location, or when the consumer is deallocated.
Also, your location manager might have been deallocated by now, so you use a weak capture group to prevent any retainment issues.
This is how the final method should look like:
func getCurrentLocation() -> Observable<CLLocation> {
let location = authorizationStatus
.filter { $0 == .authorizedWhenInUse || $0 == .authorizedAlways }
.flatMap { _ in self.didUpdateLocations.compactMap(\.first) }
.take(1)
.do(onDispose: { [weak base] in base?.stopUpdatingLocation() })
base.requestWhenInUseAuthorization()
base.startUpdatingLocation()
return location
}
How elegant! This is exactly where RxSwift shines — taking several smaller pieces and composing them into a single, cohesive and useful piece.
Updating the weather with the current data
Now that you have your getCurrentLocation() reactive extension, it’s time to put it to use. Delete the two subscriptions to geoLocationButton.rx.tap and locationManager.rx.didUpdateLocations you’ve added in the previous section.
In their place, add the following code:
let geoSearch = geoLocationButton.rx.tap
.flatMapLatest { _ in self.locationManager.rx.getCurrentLocation() }
.flatMapLatest { location in
ApiController.shared
.currentWeather(at: location.coordinate)
.catchErrorJustReturn(.dummy)
}
Upon the user’s tap on the locate button, you use the new getCurrentLocation() reactive extension you just created to get the user’s current location after asking for the proper authorization. Once you have a location, you chain another request to the OpenWeather API with its coordinates.
This makes geoSearch an observable of type Weather, which is the same result of the call made by using the city name as input. Two observables, returning the same Weather type, performing the same task… it sounds this code could be streamlined!
If you guessed it, kudos to you! Both the text search and geo search can be merged into a single observable, which will minimize your refactor efforts.
The goal is to keep search as a Driver of Weather, and running as observable of the current state of the application. To achieve the first goal, replace the current search observable with the following code:
let textSearch = searchInput.flatMap { city in
ApiController.shared
.currentWeather(for: city)
.catchErrorJustReturn(.dummy)
}
Now, you can combine textSearch with geoSearch to create a new search observable. Append after the previous block:
let search = Observable
.merge(geoSearch, textSearch)
.asDriver(onErrorJustReturn: .dummy)
This will deliver a Weather object to the UI regardless of the source, which can be either the city name or the user’s current location. The last step is to provide feedback and make sure the search displays the activity indicator correctly, hiding it after the request has been completed.
Note: You might need to move the
geoSearchobservable abovesearch, if you haven’t created it there in the first place.
Now jump to the definition of the running observable and add the locate button as a source, like so:
let running = Observable.merge(
searchInput.map { _ in true },
geoLocationButton.rx.tap.map { _ in true },
search.map { _ in false }.asObservable()
)
.startWith(true)
.asDriver(onErrorJustReturn: false)
Now, whether the user searches for the city or taps on the location button, the behavior of the application will be exactly the same.
You expanded the capability of the application, crafting a single result from multiple sources using the merge operator:
There are also some changes for the running status:
You’ve created a fairly advanced app: you started with a single text source, and you now have two data sources using the very same logic as you coded in the previous chapter.
Feel free to run your app and play around before moving to the next part.
Extending a UIKit view
Now it’s time to explore how to extend a UIKit component and go beyond what RxCocoa offers.
The application currently displays the weather at the user’s location, but it would be nice to explore the surrounding weather on a map while scrolling and navigating around.
This sounds like you will be creating another reactive extension, this time for MapKit’s MKMapView.
Extending UIKit’s MKMapView
To start extending MKMapView, you will start with the exact same pattern you used to extend CLLocationManager: create a delegate proxy RxMKMapViewDelegateProxy and extend Reactive for the MKMapView base class.
Open MKMapView+Rx.swift, found in the Extensions directory, and create the base of the extension:
extension MKMapView: HasDelegate {}
class RxMKMapViewDelegateProxy: DelegateProxy<MKMapView, MKMapViewDelegate>, DelegateProxyType, MKMapViewDelegate {
}
public extension Reactive where Base: MKMapView {
}
Inside RxMKMapViewDelegateProxy, create the initializer and the necessary reference to have the proxy in place:
weak public private(set) var mapView: MKMapView?
public init(mapView: ParentObject) {
self.mapView = mapView
super.init(parentObject: mapView,
delegateProxy: RxMKMapViewDelegateProxy.self)
}
After this, add the method to register the implementations:
static func registerKnownImplementations() {
register { RxMKMapViewDelegateProxy(mapView: $0) }
}
Next, create the proxy by adding the following to the Reactive extension:
var delegate: DelegateProxy<MKMapView, MKMapViewDelegate> {
RxMKMapViewDelegateProxy.proxy(for: base)
}
You’ve created the proxy. Now you can extend MKMapView to proxy the delegate methods as observables.
Before extending MKMapView, it’s a good idea to make sure the current project is showing the map view correctly.
There’s already a button for this in the bottom right corner of the view controller:
Add the code to viewDidLoad() to display or hide the map view when the button is pressed:
mapButton.rx.tap
.subscribe(onNext: {
self.mapView.isHidden.toggle()
})
.disposed(by: bag)
Build and run the project and repeatedly tap the map button to see the map show and hide:
Displaying overlays in the map
The map is now ready to receive and display data, but you’ll need to do a bit of work first to add the weather overlays. To add overlays to the map, you’ll implement one of its delegate methods:
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer
Wrapping a delegate that has a return type in Rx is a very hard task, for two reasons:
- Delegate methods with a return type are not meant for observation, but customization of the behavior.
- Defining an automatic default value that would work in any case is a non-trivial task.
You could observe the value using a Subject, but in this case, it would provide very little value.
Considering all these points, the best solution is to forward this call to a classic implementation of the delegate.
You’re getting the best of both worlds: you want the practicality of conforming to delegate methods with return values as you do with normal UIKit development, but you also want the ability to use observables from delegate methods. This time, for once, you can have it both ways!
MKMapViewDelegate is not the only protocol that has delegate methods requiring a return type, so there’s already a method which will help you out:
public static func installForwardDelegate(_ forwardDelegate: AnyObject, retainDelegate: Bool, onProxyForObject object: AnyObject) -> Disposable
If you want to check its implementation, look for DelegateProxyType.swift in RxCocoa.
You want to forward the delegate methods that don’t have a wrapper in the Rx proxy.
Back in MKMapView+Rx.swift, add the following to the Reactive extension for MKMapView:
func setDelegate(_ delegate: MKMapViewDelegate) -> Disposable {
RxMKMapViewDelegateProxy.installForwardDelegate(
delegate,
retainDelegate: false,
onProxyForObject: self.base
)
}
With this method, you can now install a forwarding delegate which will forward to the traditional delegate if needed.
In ViewController.swift, add the following to the end of your viewDidLoad() to set the view controller as the delegate that will receive all the non-handled calls from your RxProxy:
mapView.rx
.setDelegate(self)
.disposed(by: bag)
With this change, the compiler will raise the familiar error about the protocol not being implemented. To fix this, scroll to the end of the file and add the following:
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView,
rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
guard let overlay = overlay as? ApiController.Weather.Overlay else {
return MKOverlayRenderer()
}
return ApiController.Weather.OverlayView(overlay: overlay,
overlayIcon: overlay.icon)
}
}
OverlayView is a subclass of MKOverlayRenderer required by MKMapView to render the information on the map. The goal here is to simply display the weather icon over the map — without providing any extra information. Later in this section, you’ll revisit OverlayView in detail.
You’re almost done here: you solved the problem of the returning type of the delegate method, created a forwarding proxy, and set up the overlay to display. Now it’s time to process these overlays with RxSwift.
Navigate back to MKMapView+Rx.swift and add the following binding observer to the Reactive extension, which will take an instance of MKOverlay and inject it into the current map:
var overlay: Binder<MKOverlay> {
Binder(base) { mapView, overlay in
mapView.removeOverlays(mapView.overlays)
mapView.addOverlay(overlay)
}
}
Using Binder not only allows you to use the bind(to:) or drive methods but also makes sure you have a properly retained reference to the base - in this case, the MapView. Very convenient!
Inside the overlay binding observable, the previous overlays will be removed and the new one added every single time an overlay is sent to the Binder.
Considering the scope of the application, there’s no need for any optimization here. If there’s a need to process a large number of overlays, you could use a diffing algorithm to improve performance and reduce overhead.
Using your new binding
It’s now time to use the new Binder you’ve created. I bet you can’t wait to see it in action!
Open ApiController.swift and scroll to the end of the file and check the content of the Weather extension. There are two nested classes: Overlay and OverlayView.
Overlay is a subclass of NSObject and implements the MKOverlay protocol. This represents the information you’ll pass to OverlayView to render the actual overlay over the map. You only need to know that Overlay holds just the information necessary to display the icons in the map: the coordinates, the rectangle in which to display the data, and the actual icon to use.
OverlayView, on the other hand, is responsible for rendering the overlay. To avoid importing images, imageFromText will convert text into an image, so the icon can be displayed easily as an overlay on the map. OverlayView simply requires the original overlay instance and the icon string to create a new instance.
Inside the same Weather extension, you’ll see a convenience method that converts the structure into a valid Overlay:
func overlay() -> Overlay { ... }
Switch back to ViewController.swift and add the following code to viewDidLoad():
search
.map { $0.overlay() }
.drive(mapView.rx.overlay)
.disposed(by: bag)
This binds the newly-arrived data to the overlay binder you previously created and maps the Weather structure to the correct overlay.
Build and run, search for a city, then open the map and scroll to the city. You should see something like the following:
The result looks great, and the icon is displayed at the location of the city you searched for.
Observing for map drag events
After extending MKMapView with a binding property, it’s time to see how to implement the more conventional notification mechanism for delegates. There’s nothing different than what you did for CLLocationManager, so you can simply follow the same pattern.
On this occasion, the goal is to listen for user drag events and other navigation events from the map view. Once the user stops navigating around, you’ll update the weather condition for the middle of the map and display it.
To observe this change, MKMapViewDelegate provides the following method:
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool)
When you implement this delegate method, it is called each time the user drags the map to a new region.
This is a perfect opportunity to create a reactive extension. In MKMapView+Rx.swift, add the following inside the extension:
var regionDidChangeAnimated: ControlEvent<Bool> {
let source = delegate
.methodInvoked(#selector(MKMapViewDelegate.mapView(_:regionDidChangeAnimated:)))
.map { parameters in
return (parameters[1] as? Bool) ?? false
}
return ControlEvent(events: source)
}
In case the casting fails, the method will fall back to false, just to be safe.
Reacting to region change events
The information about the dragging is provided, and an observation mechanism using RxSwift is in place. The only missing part is to use the previously created ControlEvent.
Switch to ViewController.swift, where you will make the following changes:
- Create a
mapInput, which will use the previously created observable. - Extract
geoSearch’s input to a newgeoInput. - Update
geoSearchto mergemapInputandgeoInputtogether, so each of them will call the same weather API. - Update the
runningobservable to correctly handle the map events and weather result.
The first change is pretty straightforward and has to be done right before the let geoSearch = ... line:
let mapInput = mapView.rx.regionDidChangeAnimated
.skip(1)
.map { _ in
CLLocation(latitude: self.mapView.centerCoordinate.latitude,
longitude: self.mapView.centerCoordinate.longitude)
}
skip(1) prevents the application from firing a search right after the mapView has initialized and converting the CLLocationCoordinate2D to CLLocation will let us merge it into the existing geoSearch.
For the second change, add the following below your new mapInput:
let geoInput = geoLocationButton.rx.tap
.flatMapLatest { _ in self.locationManager.rx.getCurrentLocation() }
To put it all together, replace geoSearch with the following code to merge geoInput and mapInput:
let geoSearch = Observable.merge(geoInput, mapInput)
.flatMapLatest { location in
ApiController.shared
.currentWeather(at: location.coordinate)
.catchErrorJustReturn(.dummy)
}
You’ve created two new observables, and the only thing left to do is update the running status observable:
let running = Observable.merge(
searchInput.map { _ in true },
geoInput.map { _ in true },
mapInput.map { _ in true },
search.map { _ in false }.asObservable()
)
As before, you simply add the appropriate triggers to the merge, without changing the chained code and underlying logic.
Here’s a visual representation of the elegancy of this unidirectional data flow, where several small pieces make up the needed big pieces to drive your app:
Build and run your app, and navigate around the map to see a weather icon displaying the local weather conditions after each scroll!
Where to go from here?
In these two chapters on RxCocoa, you got a glimpse of some of the most interesting parts of this amazing extension on top of RxSwift. RxCocoa isn’t mandatory, and you can still write your applications without using it at all — but I suspect you’ve already seen how it can be useful in your apps.
Here’s a quick list of the big advantages of RxCocoa:
- It already provides an array of extensions for the most frequently-used components.
- It goes beyond basic UI components.
- It makes your code safer using Traits.
- It’s easy to use with
bind(to:)ordrive. - It provides all the mechanisms to create your own custom extensions.
Before moving on to the next chapter, play around with RxCocoa a bit to gain some confidence in using the more common extensions, as later chapters will use them fairly extensively.