Categories: Insights

Services with Reactive Observation

5 min read

When creating apps, it’s a good practice to write your business logic and interaction layers into Service Objects or Interactors.

This way, you can have them as modular components for your app, thus avoiding the repetition of code, plus you can follow single responsibility principles, and test thebehavior in an isolated fashion.

The old-school delegate way

As an example, we are going to create a WalletService, which will have the responsibility of managing current credits.

The app is not reactive yet, so we are going to sketch this interactor with delegates and try to get the job done.

We want a simple Interactor with the following features:

  1. Notifications on updates
  2. The increase credits method
  3. The use credits method

Define the protocol for the service

You can do this by implementing the following:

public protocol WalletServiceDelegate {
funccreditsUpdated(credits: Int)
}

The first requirement is now defined.

Create the service

Now you can create the service:

public class WalletService {
    public var delegate: WalletServiceDelegate?
    private (set) var credits: Int

    public init(initialCredits: Int = 0) {
        self.credits = initialCredits
    }

    public func increase(quantity: Int) {
        credits += quantity
    }

    public func use(quantity: Int) {
        credits -= quantity
    }
}

With these few lines, our basic requirements have been met.

Ready for use

But, we are using delegates and hence we will need to create an object so that our delegate protocol can use this interface! This will be sufficient for our needs at the moment:

class Foo: WalletServiceDelegate {
    func creditsUpdated(credits: Int) {
        print(credits)
    }
}

let service = WalletService()
let myDelegate = Foo()
service.delegate = myDelegate

Well, while this is working, we need to use WalletService in more parts of the project.

That means rewriting the actual code to work as a program class with static vars and class functions. It also needs to support multiple delegate subscriptions (adding and removing them too).

That will mean a really complex code for a really simple service, and this problem will be repeated all over your shared services.

There’s a framework for this called RxSwift!

The RxSwift way

Our code will look like the following after removing the delegate dependency:

public class WalletService {
    private (set) var credits: Int

    init(initialCredits: Int = 0) {
        self.credits = initialCredits
    }

    public func increase(quantity: Int) {
        credits += quantity
    }

    public func use(quantity: Int) {
        credits -= quantity
    }
}

We want to operate this as a program class, so we will make a way for that with RxSwift.

Rewriting our code with RxSwift

When you dig into RxSwift’s units, you realize that Variable is the unit that fits the most requirements, and you can easily mutate its value and subscribe to changes easily.

If you contain this unit in a public and static variable, you will be able to operate these services as a program class:

import RxSwift
import RxCocoa

public class WalletService {
    private (set) static var credits = Variable<Int>(0)

    public class func increase(quantity: Int) {
        credits.value += quantity
    }

    public class func use(quantity: Int) {
        credits.value -= quantity
    }
}

The result of this is great, simple code that is easy to operate, with no protocoland no instanciable classes.

Usage with RxSwift

Let’s subscribe to credit changes. You need to use the Variable as a Driver or an Observable. We will use it as a driver:

let disposeBag = DisposeBag()
// First subscription
WalletService.credits.asDriver()
    .driveNext { print($0) }
    .addDisposableTo(disposeBag)
// Second subscription
WalletService.credits.asDriver()
    .driveNext { print("Second: ($0)") }
    .addDisposableTo(disposeBag)
WalletService.increase(10)
WalletService.use(5)

This clearly shows a lot of advantages. We didn’t depend on a class, and we can add as many subscriptions as we want!

As you can see, RxSwift helps you make cleaner services/interactors with a simple API as higher functionality.

With this pattern, you can have subscriptions over the app to changing data, so we will navigate through the app and have all of the views updated with the last changes without forcing a redraw of the view for every update in the dependency data.

A higher complexity example

Dealing with geocalization is tough. It’s a reality, but there are ways to make interoperability with it easier.

To avoid future problems and repeated or messy code, create a workaround:CoreLocation. With RxSwift, you have easier access to subscribe to CoreLocation updates, but I consider that approach not good enough.

In this case, I recomend making a class around CoreLocation, using it as a shared Instance, to manage the geolocation updates with a global interoperability allowing upi to pause or start updates without much code:

import RxSwift
import RxCocoa
import CoreLocation

public class GeolocationService {
    static let sharedInstance = GeolocationService()

    // Using the explicit operator is not a good practice,
    // but we are 100% sure we are setting a driver into this variables on init.
    // If we don't do that, the compiler wont pass, even thought, it works.
    // Apple might do something here for the future versions of Swift.

    private (set) var authorizationStatus: Driver<CLAuthorizationStatus>!
    private (set) var location: Driver<CLLocation>!
    private (set) var heading: Driver<CLHeading>!

    private let locationManager = CLLocationManager()

    init() {
        locationManager.distanceFilter = kCLDistanceFilterNone
        locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
        authorizationStatus = bindAuthorizationStatus()
        heading = bindHeading()
        location = bindLocation()
    }

    public func requestAuthorization() {
        locationManager.requestAlwaysAuthorization()
    }

    public func startUpdatingLocation() {
        locationManager.startUpdatingLocation()
        locationManager.startUpdatingHeading()
    }

    public func stopUpdatingLocation() {
        locationManager.stopUpdatingLocation()
        locationManager.stopUpdatingHeading()
    }

    private func bindHeading() -> Driver<CLHeading> {
        return locationManager.rx_didUpdateHeading
            .asDriver(onErrorDriveWith: Driver.empty())
    }

    private func bindLocation() -> Driver<CLLocation> {
        return locationManager.rx_didUpdateLocations
            .asDriver(onErrorJustReturn: [])
            .flatMapLatest { $0.last.map(Driver.just) ?? Driver.empty() }
    }

    private func bindAuthorizationStatus() -> Driver<CLAuthorizationStatus> {
        weak var wLocationManager = self.locationManager
        return Observable.deferred {
            let status = CLLocationManager.authorizationStatus()
            guard let strongLocationManager = wLocationManager else {
                return Observable.just(status)
            }
            return strongLocationManager
                .rx_didChangeAuthorizationStatus
                .startWith(status)
        }.asDriver(onErrorJustReturn: CLAuthorizationStatus.NotDetermined)
    }
}

As result of this abstraction, we now have easy operability over geolocation with just subscriptions to our exposed variables.

About the author

Alejandro Perezpaya has been writing code since he was 14. He has been developing a multidisciplined profile, working as an iOS Developer but also as a Backend and Web Developer formultiple companies. After working for years in Madrid and New York startups (Fever, Cabify, Ermes), he started Triangle, which is a studio based in Madrid, a few months ago, where he crafts high quality software products.

Alejandro Perezpaya

Share
Published by
Alejandro Perezpaya

Recent Posts

Top life hacks for prepping for your IT certification exam

I remember deciding to pursue my first IT certification, the CompTIA A+. I had signed…

3 years ago

Learn Transformers for Natural Language Processing with Denis Rothman

Key takeaways The transformer architecture has proved to be revolutionary in outperforming the classical RNN…

3 years ago

Learning Essential Linux Commands for Navigating the Shell Effectively

Once we learn how to deploy an Ubuntu server, how to manage users, and how…

3 years ago

Clean Coding in Python with Mariano Anaya

Key-takeaways:   Clean code isn’t just a nice thing to have or a luxury in software projects; it's a necessity. If we…

3 years ago

Exploring Forms in Angular – types, benefits and differences   

While developing a web application, or setting dynamic pages and meta tags we need to deal with…

3 years ago

Gain Practical Expertise with the Latest Edition of Software Architecture with C# 9 and .NET 5

Software architecture is one of the most discussed topics in the software industry today, and…

3 years ago