Why you should abstract any iOS third party libraries

If you have an iOS app, you might have integrated external libraries and tools to help you getting your product ready faster. However your iOS architecture and swift code shouldn’t depend on those libraries.

As many iOS developers, you have implemented external services to your app. It could be a payment system like Stripe, a monitoring tool like NewRelic or Crashlytics, or maybe a tracking platform like Google Analytics.

Those tools are amazing, but let’s say you have now to switch to a new service. Does it mean you have hours to refactor your approach? Would you have to rewrite hundreds of lines of code? If yes, then that’s the reason we should always abstract your implementation for external services.

ios-swift-abstract-libraries

In this demo, I show from a Firebase integration how to switch it to Fabric for logging events. This approach include how to abstract the implementation to contain third-party libraries into our code and make it easier to migrate.

To prepare our code for this migration, we need to abstract couple elements: our events to send, our loggers and we might also need a manager to keep everything under a singleton.

Assuming we have 3 classic events, landing on home page, signing up and logging in. My view might look like that

import UIKit
import Firebase

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        FirebaseApp.configure()
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        Analytics.logEvent("homepage", parameters: nil)
    }

    func didLogin() {
        Analytics.logEvent("login", parameters: ["userID" : "1234567890ABC"])
    }

    func didSignUp() {
        Analytics.logEvent("signup", parameters: ["userID" : "1234567890ABC"])
    }
}

That’s what we would like to avoid. Here if we change for a new tool, we have a lot of work to move, and probably the same approach for each pages.

Let’s abstract our Firebase implementation

enum Events : String {
    case login
    case home
    case sign_up
}

protocol LoggerProtocol {
    func setup()
    func log(event: Events, parameters: [String: Any]?)
}

Here I just designed events I want to recognise and a protocol to make it generic across the app. I’m ready to reimplement Firebase the right way.

import Firebase

struct FirebaseLogger : LoggerProtocol {

    func setup() {
        FirebaseApp.configure()
    }

    func log(event: Events, parameters: [String : Any]? = nil) {
        Analytics.logEvent(event.rawValue, parameters: parameters)
    }
}

I also created a LoggerManager to keep every setup and logger under the same umbrella, using a singleton to get access to it.

class LoggerManager : LoggerProtocol {

    static let shared : LoggerManager = {
        var manager = LoggerManager()
        manager.loggers.append(FirebaseLogger())
        manager.setup()
        return manager
    }()

    private var loggers : [LoggerProtocol] = []

    internal func setup() {
        self.loggers.forEach({ $0.setup() })
    }

    func log(event: Events, parameters: [String : Any]? = nil) {
        self.loggers.forEach({ $0.log(event: event, parameters: parameters) })
    }
}

Let’s now update our view, removing as much as possible dependencies from the tool itself.

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        LoggerManager.shared.log(event: .home)
    }

    func didLogin() {
        LoggerManager.shared.log(event: .login, parameters: ["userID" : "1234567890ABC"])
    }

    func didSignUp() {
        LoggerManager.shared.log(event: .signup, parameters: ["userID" : "1234567890ABC"])
    }
}

It looks like we’re good to go, no more dependency to Firebase in our view.

We’re good to migrate to any new tools.

Here is my Fabric logger

import Fabric
import Answers

struct FabricLogger : LoggerProtocol {

    func setup() {
        Fabric.with([Answers.self])
    }

    func log(event: Events, parameters: [String : Any]?) {
        Answers.logCustomEvent(withName: event.rawValue, customAttributes: parameters)
    }
}

Don’t forget to update your LoggerManager, remove Firebase one for Fabric.

static let shared : LoggerManager = {
    var manager = LoggerManager()
    manager.loggers.append(FabricLogger())
    manager.setup()
    return manager
}()

As you can see the code is working as before and we didn’t have to update the view side.

In conclusion, we manage to abstract our third-party library integrations to make it easier to maintain and to reuse. We could actually keep Firebase and Fabric together, and limit their logging if we had a specific approach.

My main goal here was to show you how to avoid your iOS projects and swift code being dependant of other tools. If not convinced, another good reason is it’s time saving. You don’t want to spend couple days every year to re-implement a new tool depending on what’s new on the market.

© 2023 Benoit Pasquier. All Rights Reserved
Author's picture

Benoit Pasquier

Software Engineer 🇫🇷, writing about career development, mobile engineering and self-improvement

ShopBack 💰

Singapore 🇸🇬