Implementing qualitative analytics for your SwiftUI app with PostHog

Colin Wren
4 min readNov 29, 2024

--

Photo by Choong Deng Xiang on Unsplash

As I start to build my career-focused reflective journaling app Garner, my attention has turned to how I’m going to understand what works and what isn’t working in the initial feature set of my MVP.

When creating my MVP’s feature set I’ve scaled it right down to the core functionality needed to complete an end-to-end journey of my idea. The intention being that if I keep that as simple as possible it’s easier for those using the app to explore it, see how awesome it is and either continue to use it or give me feedback on why they wouldn’t want to use it.

Once I open up the public beta for Garner it won’t be easy for me to sit with those taking their first steps with the app to see how they use it so I’ll need to find a way of remotely identifying the usage of the app and the way I’ve done this in the past is to use PostHog.

PostHog is an analytics platform that I’ve used in my previous app Reciprocal.dev to measure how effective the onboarding flow was, focusing on qualitative and not quantitive analytics which was something that PostHog excelled in.

Reciprocal.dev was built as a web app however and Garner is going to be a native SwiftUI app for iOS, iPadOS and MacOS so I was worried I wasn’t able to use PostHog again but luckily they have a swift library for native apps.

Setting up PostHog for a SwiftUI app

After creating a new project in PostHog for Garner I followed the instructions to create the API key needed for use with the library.

I then installed the PostHog via the package management UI in Xcode using their Github URL as per the PostHog iOS docs https://posthog.com/docs/libraries/ios

I then added the configuration code as per the docs to my app’s initialiser:

Setting up PostHogSDK during app initialisation

Capturing events with PostHog

By default PostHog will automatically capture a lot of events for you such as when a Screen is viewed, the app is installed, opened, closed and elements on the Screen are interacted with. The auto-capture of events can be turned off during the initialisation of the SDK but I decided to keep the defaults.

Capturing Screen views

The PostHog documentation suggests that adding the .postHogScreenView()view modifier to your screens will provide your events with the name of the screen based on the struct’s name in Swift but I found this didn’t work for me.

Instead I had screen view events with screen names such as the very descriptive UIHostingController<ModifiedContent<AnyView, RootModifier>>, I think this may be due to my app having a TabViewas the root view. You can add the name of the screen as an argument of .postHogScreenView()so that allowed me to clean this up.

As I was using tabs I also wanted to track which tabs were being navigated between as the onboarding flow so I added an .onChange()view modifier to my TabViewfor this.

Passing screen names to PostHog screen view

Capturing user events

To capture user events you call PostHogSDK.shared.capturewith an event name and optional propertiesdictionary.

However once you start capturing multiple events it can be hard to keep track of all the strings you have for the event names and you have a lot of boiler plate code.

To get around this I used Swift’s magical enum data type to give me a nice clean abstraction that made it easy to use autocomplete for the events and to remove all the duplicatePostHogSDK.shared.capturestatements from my code.

Using Swift enum to provide a cleaner analytics interface

This means that my call site now looks much cleaner:

using the analytics enum in application code

And it has the benefit that if I need to update the value sent to PostHog at a later point in time I only have to make that change in one place.

Funnels and analysis in PostHog

Once the analytics events have been sent to PostHog the next step is to use that data to build a visualisation to aid analysis. I won’t go into all the details of how to select the values and create a funnel in PostHog but there’s three main “steps” you’ll use when creating a funnel:

  • Automatic app life cycle events like “Application Opened’
  • Screen events — You’ll want to filter these based on Screen Name using the values set in .postHogScreenView
  • Captured events — The events captured via PostHogSDK.shared.capture

For Garner I’ve defined the features of my app in Gherkin, a text based format for defining the context a user is in, the action they take and how the application behaves. I found it useful to re-use these definitions for my analytics.

For example I have a Scenario for adding an update:

Given the User is on the Update Screen
When they add an Update
Then the System creates a new Update using the currently active Update Configuration
And navigates the User to the Update Form for the newly created Update

This can then translate to the following funnel of events:

  • Screen (name: “Update List”)
  • Event (“UpdateList.addUpdate”, properties: [“from”: “Header”])
  • Screen (name: “Update Detail”)

I also plan to use the Gherkin as part of my test automation strategy so I could then also check that the appropriate events are being sent as part of the scenario execution which would be a good way of catching if I accidentally stop capturing events that could skew the data in PostHog.

Next steps

Now I’ve got an insight into how users are using Garner based on the feature set I’ve defined for the MVP I can now think about how that data is going to help guide my future development efforts.

I’ve been reading up on the Product-Led Growth Flywheel lately and while my MVP is scoped to show the end-to-end value of Garner I think it’s likely that as I frame the analytical data I collect against that Flywheel I’ll be able to see areas that are preventing Explorers of the app from becoming Champions.

--

--

Colin Wren
Colin Wren

Written by Colin Wren

Currently building reciprocal.dev. Interested in building shared understanding, Automated Testing, Dev practises, Metal, Chiptune. All views my own.

Responses (1)