Architecting an Analytics Service for iOS Apps

Monitoring the behavior of your app's users is critical to the success of your app. If the only feedback you get from your users are App Store review, now is the right time to start using analytics. We'll go over how you create an analytics service that's modular and easily extensible using a clean iOS architecture.

Written by 

Analytics is where you add certain events in your app to collect information about the user behavior within your app. This allows for determining whether something is a good idea based on real-life data. You can easily track if a feature is being used, at which point in a feature flow most users drop out, etc. while respecting the users' privacy by only collecting anonymous data.

Overview of the system

Let's have a brief overview of what we're going to implement. For our solution, we're going to have a few goals:

  • It should be easy to use analytics from any view controller. You should only need one line of code to add an analytics event.
  • It should support any underlying provider of analytics (Firebase Analytics, Mixpanel, etc.)
  • It should be easy to add, remove & modify events and they should be statically typed.
  • It should be scalable and applicable even for enterprise apps.

We're looking for achieving a highly scalable solution that makes it easy to add new events without the need to recompile the entire service. For this, I propose an architecture with modularization and the right level of abstractions. Take a look at the following diagram:

This architecture will get the job done. The view controllers in our app should only know about the analytics service which acts as an abstraction to the underlying providers there may be. By using extensions in each of our feature modules (e.g. CategoryListViewController and ItemListViewController are kept in separate modules), we can easily add new features to the system that are statically typed.

Analytics Service

We'll begin with the models. First off, our analytics events will be based on this AnalyticsEvent struct. It contains a name and parameters which we can use to pass extra information into our events.

public struct AnalyticsEvent {
    var name: String 
    var parameters: [String: String] 
    
    init(name: String, parameters: [String: String] = [:]) {
        self.name = name self.parameters = parameters
    }
}

Now, onto the analytics service that the view controllers interact with:

public final class AnalyticsService {
    private static var providers = [AnalyticsProvider]()
    
    static let shared = AnalyticsService(providers: providers) 
    
    private init(providers: [AnalyticsProvider]) {
        AnalyticsService.providers = providers
    }

    static func register(provider: AnalyticsProvider) {
        providers.append(provider)
    }

    func report(event: AnalyticsEvent) {
        AnalyticsService.providers.forEach {
            $0.reportEvent(name: event.name, parameters: event.parameters)
        }
    }
}

The analytics service has a function report to send off an analytics event to any registered providers. Initially, you should call the register function to add the analytics provider(s) you want to send events to.

Analytics Providers

You need to implement the analytics provider you want to go with. This could be a SAAS solution such as Firebase Analytics for collecting analytics events in.

This is the protocol that all providers should conform to:

public protocol AnalyticsProvider {
    func reportEvent(name: String, parameters: [String: String])
}

This FirebaseAnalyticsProvider struct is the concrete implementation of the protocol based on Firebase Analytics. If you have Firebase Analytics added to your project, you can add it as an analytics provider like this:

public struct FirebaseAnalyticsProvider: AnalyticsProvider {
    private let eventMapper: AnalyticsEventMapper init(eventMapper: AnalyticsEventMapper) {
        self.eventMapper = eventMapper setup()
    }

    func setup() {
        FirebaseApp.configure()
    }

    public func reportEvent(name: String, parameters: [String: String]) {
        let mappedName = eventMapper.name(for: name) let mappedParameters = eventMapper.parameters(for: parameters) Analytics.logEvent(mappedName, parameters: mappedParameters)
    }
}

You also need to pass an event mapper to your provider. Certain analytics providers use certain formats of event names and parameters. Events in Firebase Analytics are often written in snake case format, so we'll add an event mapper to transform our event name and parameters to this format. First off, we need a protocol:

public protocol AnalyticsEventMapper {
    func name(for event: String) -> String 
    func parameters(for parameters: [String: String]) -> [String: String]
}

The concrete event mapper used by the FirebaseAnalyticsProvider should look like this:

public struct SnakeCaseAnalyticsEventMapper: AnalyticsEventMapper {
    public func name(for eventName: String) -> String {
        return String(describing: eventName).camelCaseToSnakeCase()
    }

    public func parameters(for parameters: [String: String]) -> [String: String] {
        return Dictionary(uniqueKeysWithValues: parameters.map {
            ($0.key.camelCaseToSnakeCase(), $0.value)
        })
    }
}

fileprivate extension String {
    func camelCaseToSnakeCase() -> String {
        let acronymPattern = "([A-Z]+)([A-Z][a-z]|[0-9])" let normalPattern = "([a-z0-9])([A-Z])" return self.processCamalCaseRegex(pattern: acronymPattern)? .processCamalCaseRegex(pattern: normalPattern)?.lowercased() ?? self.lowercased()
    }

    private func processCamalCaseRegex(pattern: String) -> String? {
        let regex = try? NSRegularExpression(pattern: pattern, options: []) let range = NSRange(location: 0, length: count) return regex?.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "$1_$2")
    }
}

That's it. Next, we'll look at how you use this in your project.

Usage

When your application launches, you want to register your analytics provider(s) and pass the analytics service to one of your view controllers to be used.

Here's an example where this is done in SceneDelegate:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow? 
    
    func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
        .. scene setup ...
        
        let snakeCaseEventMapper = SnakeCaseAnalyticsEventMapper() 
        AnalyticsService.register(provider: FirebaseAnalyticsProvider(eventMapper: snakeCaseEventMapper)) 
        let categoryVC = CategoryViewController(analyticsService: .shared)
        
        window.rootViewController = UINavigationController(rootViewController: categoryVC) 
        window.makeKeyAndVisible()
    }
}

Lastly, you can add your analytics events used by each feature module with an extension:

extension AnalyticsEvent {
    static let categorySelected = AnalyticsEvent(name: "categorySelected") 
    static func categoryAdded(categoryTitle: String) -> AnalyticsEvent {
        return AnalyticsEvent(name: "categoryAdded", parameters: ["categoryTitle": categoryTitle])
    }
}

Now, we can use the analytics service to report an analytics event in a statically typed manner. In this example, we are tracking an event when a new category is being added in a todo app:

guard let categoryTitle = textField.text else { return }

let category = Category(title: categoryTitle) 
categories.append(category) 

analyticsService.report(event: .categoryAdded(categoryTitle: categoryTitle))

Go ahead and try it out. You should be able to report any event and see it in your analytics provider's dashboard.

Conclusion

We looked at the benefits of analytics and how to use analytics for gathering all kinds of data about your users' behavior in your app. You can check out the source code that we covered here. Note that the sample code is a basic todo app without any modularization matching the diagram. That's an exercise for you when you implement this.

Do you want to become an iOS architect? Check out iOS Architect Accelerator.

Share this post

Facebook
Twitter
LinkedIn
Reddit

You may also like

Automation

iOS Continuous Deployment Using Travis CI and Fastlane

Continuous Deployment (CD) is a practice of using automation to release your code in a quick and sustainable way. Here’s how you set up CD for iOS apps to perform automated building, code signing and deployment to the App Store.

Automation

iOS Continuous Integration Using Travis CI and Fastlane

Continuous Integration (CI) is a powerful practice to keep any app in a stable state throughout its development. Here’s how you set up CI for iOS apps to automatically perform code validation and testing whenever your code changes.

Comparison

Native App vs React Native App – What Should You Choose?

Gone are the days where the only options in mobile app development were native iOS and Android. The choices are broader nowadays, and frameworks have popped up with React Native being the most popular alternative. So what approach should you choose for your next app?