Skip to main content

In-app analytics for iOS apps: Architecture tips

In the last few months we added a third party app analytics solution i.e. Firebase to My Day To-Do and soon we realised that Firebase may not work every time. Hence we tried to abstract the analytics (decouple/separate) solution from the rest of the app code i.e. the app code can log analytics data without knowing about what analytics solution is in use. In this post I will talk about our analytics solution and provide some code examples for an iOS app with Firebase analytics. The aim of the code is to provide an example of how to abstract the analytics part from the rest of the app. Hence without further a due let's get to it.

Background


In June 2017, we added a third-party analytics solution i.e. Firebase analytics to My Day To-Do for tracking app usage i.e. we wanted to know what aspects of our product do people use the most. At this stage I am the only developer at My Day To-Do and it was the first time I had worked with an analytics solution. Anyway I added an analytics solution and it was all fine and dandy but then I relied that Firebase may not work in all situations (e.g. China). At this point, I thought why not abstract the analytics solution from the rest of the app, so the app code does not care about what analytics solution is being used. This lead me to  embark on a quest to restructure the app analytics solution in My Day To-Do.

Problem


Design an analytics solution in the app such that you can change the existing (third-party?) analytics solution without effecting the code for the rest of the app.


Solution

First let's examine how Firebase Analytics was initially integrated into My Day To-Do,

Firebase and My Day To-Do


Firebase Analytics let's you log events and when I first added it to my app, I had  a method called logEventWithFirebase in one of the classes coupled with the rest of the app code. The Firebase event that I used the most was select_content which accepts 3 parameters (id, type, name). The code to log a select_content event is the same every time, the only thing that changes are the 3 parameters, so when designing that I thought let's just store the 3 parameter values for different select_content events in one location. 

Analytics Constants


My goal for this solution was to have a file that houses (stores) all the different events that we are logging making it easier to track the events being logged. To accomplish that goal, I created a class called AnalyticsConstants where I defined a struct called ANALYTICS_CONSTANT with three properties id, name and type. The AnalyticsConstants class has several static properties of type ANALYTICS_CONSTANT that correspond to the events we are logging with Firebase. Confusing? yeah I am confused by that statement too, so how about we just look at some code to get an idea

struct ANALYTICS_CONSTANT {
    var id = ""
    var type = ""
    var name = ""
    init(id:String, name: String, type: String) {
        self.id = id
        self.name = name
        self.type = type
    }
}
static let A_TODO_VISIT = ANALYTICS_CONSTANT(id: "a_tab", name: "TodoTabVisit", type: "PageView")
btw that's Swift code from the My Day To-Do iOS app 

this is what the logEventForFirebase function look like,

func logEventForFirebase(analyticsConstant: AnalyticsConstants.ANALYTICS_CONSTANT) 
{
    Analytics.logEvent(AnalyticsEventSelectContent, parameters: [
        AnalyticsParameterItemName: analyticsConstant.name as NSObject,
        AnalyticsParameterItemID: "id-\(analyticsConstant.id)" as NSObject,
        AnalyticsParameterContentType:  analyticsConstant.type as NSObject
    ])
}
and now for how it's called in code

logEventForFirebase(AnalyticsConstants.A_TODO_VISIT)

At this point we have events being logged with Firebase, all the different moving parts are in place and it all works, but I noticed something. I noticed that despite a large number of downloads from China the app usage in China is fairly insignificant compared to say 'Germany' which has far fewer downloads. The reason being that Firebase Analytics is not accurate when it comes to reporting app usage in China, therefore I may need to change the analytics solution from Firebase to something else. At this point, I decided to re-think this entire process, actually let's go back to the basics and examine this entire process starting with...

Why we build apps? 
  • to solve a problem 
  • so an app is a solution to a problem 

Fantastic, so how do we build apps?
  • we design the UI 
  • code the logical app flow and 
  • add a means of storing data etc etc
Now we can look at the UI design, logical app flow, storing data etc as smaller problems that we need to solve to solve a bigger problem i.e. building an app. Therefore it's not too difficult to think of our app as a big solution composed of a number of smaller solutions, consequently we can see adding app analytics as another smaller solution.

A central location


After thinking about it that way, it would make sense to add analytics to our app via a single class that stores all the methods to log analytics data which the rest of the app code can call. This leads to the thought of having the AnalyticsHelper, which looks like this,

static func logEvent(analyticsConstant: AnalyticsConstants.ANALYTICS_CONSTANT) 
{

    Analytics.logEvent(AnalyticsEventSelectContent, parameters: [
        AnalyticsParameterItemName: analyticsConstant.name as NSObject,
        AnalyticsParameterItemID: "id-\(analyticsConstant.id)" as NSObject,
        AnalyticsParameterContentType:  analyticsConstant.type as NSObject
    ])
}
So whenever we need to log an event for our app, all we do is call AnalyticsHelper.logEvent that's all. The logEvent method can log the event to any analytics service, it could be Firebase, Fabric, Google analytics or whichever other solution, the app code does not need to know that. 

Having said that the app does have one dependency and that's AnalyticsConstants, but that's unavoidable and needless to say that AnalyticsHelper and AnalyticsConstants need to know about each other. Therefore it would only make sense to store them under the same folder for ease of understanding, so let's create a folder in our app called Analytics and have both the files in there.

How does it all fit together?

In the interest of clarity I think it would make sense to look how all this code fits together in an iOS app, let's look at our viewController

import UIKit
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    @IBAction func todoVisit(_ sender: Any) {
        AnalyticsHelper.logEvent(AnalyticsConstants.A_TODO_VIST)
    }
}
It's fairly self-explanatory, it's has a an event that's triggered when the button is pressed and we are logging that event using our AnalyticsHelper. Next let's look at our AnalyticsConstants class

import Firebase
class AnalyticsHelper {

    static func logEvent(analyticsConstant: AnalyticsConstants.ANALYTICS_CONSTANT) 
    {

        Analytics.logEvent(AnalyticsEventSelectContent, parameters: [
           AnalyticsParameterItemName: analyticsConstant.name as NSObject,
           AnalyticsParameterItemID: "id-\(analyticsConstant.id)" as NSObject,
           AnalyticsParameterContentType:  analyticsConstant.type as NSObject
        ])
    }
}
and finally the AnalyticsConstants class

import Foundation
/*
 Acronyms
 A = access i.e. access a section
 P = push i.e. button push 
 */
class AnalyticsConstants {

    struct ANALYTICS_CONSTANT {
        var id = ""
        var type = ""
        var name = ""
        init(id:String, name: String, type: String) {
            self.id = id
            self.name = name
            self.type = type
        }
    }
    static let A_TODO_VISIT = ANALYTICS_CONSTANT(id: "a_tab", name: "TodoTabVisit", type: "PageView")
    static let P_LOGIN = ANALYTICS_CONSTANT(id: "p_login", name: "Login", type: "Button")
}

The only additional property is if we were tracking user login, also I have just named the variables with the starting letter for the type of event it's associated to i.e. "button push" and "access" certain sections of the app.

p.s. if you need to know more about that entire China situation, My Day To-Do download numbers and Firebase Analytics, you can read my previous blogpost where it's extensively discussed.

Summary


I would encourage anyone adding an analytics solution to their app to use or at least consider a viable alternative to the solution discussed here. The feasibility of a third-party analytics solution for a product can change over time therefore I think it's best to try and prepare for this by ensuring that the Analytics solution code is not tightly coupled with the app code. 

Finally, I am working on my app full-time right now so if you find my blog posts useful and want to support me you can 

    • Or download the Lite(free) version and leave us an App Store review

    Comments

    Popular posts from this blog

    Upload to AWS S3 from Java API

    In this post, you will see code samples for how to upload a file to AWS S3 bucket from a Java Spring Boot app. The code you will see here is from one of my open-source repositories on Github, called document-sharing. Problem Let’s say you are building a document sharing app where you allow your users to upload the file to a public cloud solution. Now, let’s say you are building the API for your app with Spring Boot and you are using AWS S3 as your public cloud solution. How would you do that? This blog post contains the code that can help you achieve that. Read more below,  Upload to AWS S3 bucket from Java Spring Boot app - My Day To-Do (mydaytodo.com)

    Addressing app review rejections for auto-renewing subscription in-app purchase (iOS)

    The ability to know what the weather is like while planning your day is a feature of  My Day To-Do  Pro and as of the last update it’s also a part of the  Lite version . Unlike the Pro version it’s an auto-renewing subscription based  in-app purchase (IAP)  in the Lite version. What means is that when a user purchases it, the user only pays for the subscription duration after which the user will be automatically charged for the next period. Adding an  auto-renewing  subscription based IAP proved to be somewhat challenging in terms of the app store review i.e. the app update was rejected by the App Review team thrice because of missing information about the IAP. Therefore in this post I will share my experiences and knowledge of adding auto-renewing IAP in hopes to save someone else the time that I had to spend on this problem. In-App purchase This year I started adding IAPs to My Day To-Do Lite which lead to learning about different types of IAP...

    Getting started with iOS programming using Swift (Part 1)

    I have not been too fond of Objective-C, which was the primary reason for me to stay away from making iOS apps till now. So what changed? Well Apple has done something very interesting recently and that is the introduction of a new programming language i.e. Swift. Swift is awesome, it almost feels like Python, C++ and Objective-C had a baby with some of their good parts in them. So I have been getting to know Swift and it is an awesome language to program in. What I am going to share with this and a series of blog posts are solutions to some problems that i have encounter while i am trying to finish my first iOS app. The one hurdle that I have encountered while getting started on developing an iOS app is that a majority of the solutions for iOS specific problems provide solutions to them using Objective-C. Which is fair, because Swift has not been around for that long. Anyway let us get started with a few basics, A few basics I would highly recommend having a read of this book...