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.
Architecting an Analytics Service for iOS Apps
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
Design
How to Make Animated 3D Mockups in Seconds
As an individual app developer, you need to bring attention to your published apps. Companies of top-selling apps agree on how this is executed successfully. They create stunning and professional-looking animated 3D mockups of the apps. I’m here to tell you how you can create these for yourself in a matter of seconds.
Process
Eliminate Crashes & Incidents in Mobile Development Teams
Defect management should not be a phase in the lifecycle of an app, but instead a fundamental part of an agile process. Here is my recommendation of processes to enforce defect management into your team’s daily workflow.
DevOps
3 EAS Pipelines for Deploying React Native Apps in Teams
One of the best ways to increase productivity in a react native project is to automate the process of deploying builds you want to share internally in your team or with actual costumers via the app store. And you might be surprised at how easy it can actually be implemented when you have the right set of tools like GitHub Actions and EAS.