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.
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:
You can do this by implementing the following:
public protocol WalletServiceDelegate {
funccreditsUpdated(credits: Int)
}
The first requirement is now defined.
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.
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!
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.
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.
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.
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.
I remember deciding to pursue my first IT certification, the CompTIA A+. I had signed…
Key takeaways The transformer architecture has proved to be revolutionary in outperforming the classical RNN…
Once we learn how to deploy an Ubuntu server, how to manage users, and how…
Key-takeaways: Clean code isn’t just a nice thing to have or a luxury in software projects; it's a necessity. If we…
While developing a web application, or setting dynamic pages and meta tags we need to deal with…
Software architecture is one of the most discussed topics in the software industry today, and…