What's New

Initial Release


This is the initial release of the Composable Analytics package. It includes...

  • AnalyticsData
  • AnalyticsClient (with the .consoleLogger) implementation.
  • AnalyticsRedcuer

It provides easy testing through the user of expect on the dependency.

And multiple clients can be merged using the .merge function.

Composable Analytics

A composable and decoupled way to add analytics to any TCA project without getting the analytics and working code tangled up together.

Basic Usage

Composable Analytics provides an AnalyticsReducer which provides all the working logic for unwrapping and sending your analytics events to the @Dependency(\.analyticsClient) in your project. By default the analyticsClient dependency is set to unimplemented so first you should add the dependency to your store.

At the entry point of your app when you first create the Store you can update the analytics here. This package provides a .consoleLogger client and you can add your own too.

  initialState: App.State(),
  reducer: App()
    .dependency(\.analyticsClient, AnalyticsClient.consoleLogger)

Then in any Reducer within the app you can add an AnalyticsReducer to the body. This is created with a function that takes state and action and returns an optional AnalyticsData.

struct App: Reducer {
  struct State {
    var title: String

  enum Action {
    case buttonTapped

  var body: some ReducerOf<Self> {
    AnalyticsReducer { state, action in
      // state here is immutable so there is no way for your analytics to interfere with your app.
      switch action {
      case .buttonTapped:
        return  .event(name: "AppButtonTapped", properties: ["title": state.title])
    Reduce<State, Action> { state, action in
      // your normal app logic sits here unchanged

This keeps all of your analytics out of your working code but still in a place that makes it easy to see and reason about what analytics your app is sending.

As most analytics will probably be events without any properties the AnalyticsData is expressible by string literal. So, .event(name: "SomeName") and "SomeName" are equivalent.

As a personal preference, I tend to use default: return nil at the end of it. nil is returned from the AnalyticsReducer for any action/state combination when you don't want it to send analytics. So it is a lot more convenient to wrap them all up in a default case at the end of the switch rather than list out all the actions and return nil from each.

Custom Analytics Clients

This package only provides an analytics client for logging to the console. Accessible as AnalyticsClient.consoleLogger but you can very easily add your own custom clients.

For example, you may want to log analytics to Firebase. In which case you can add your own clients be extending AnalyticsClient...

import Firebase
import FirebaseCrashlytics
import ComposableAnalytics

public extension AnalyticsClient {
  static var firebaseClient: Self {
    return .init(
      sendAnalytics: { analyticsData in
        switch analyticsData {
        case let .event(name: name, properties: properties):
          Firebase.Analytics.logEvent(name, parameters: properties)

        case .userId(let id):

        case let .userProperty(name: name, value: value):
          Firebase.Analytics.setUserProperty(value, forName: name)

        case .screen(name: let name):
          Firebase.Analytics.logEvent(AnalyticsEventScreenView, parameters: [
            AnalyticsParameterScreenName: name

        case .error(let error):
          Crashlytics.crashlytics().record(error: error)

This could be your Firebase implementation. Which you then add to the store by merging with any other clients you want to use...

let analytics = AnalyticsClient.merge(
  // this merges multiple analytics clients into a single instance

  initialState: App.State(),
  reducer: App()
    .dependency(\.analyticsClient, analytics)


This leans into the TCA way of testing. Because all your analytics are sent using Effects. This package provides an expect function that can be used to easily tell your test which analytics you are expecting during a test...

import XCTest
import ComposableArchitecture
@testable import App

class AppTests: XCTestCase {
  func testButtonTap() async throws {
    let store = TestStore(
      initialState: App.State.init(title: "Hello, world!"),
      reducer: App()

      .event(name: "AppButtonTapped", properties: ["title": "Hello, world!"])

    await store.send(.buttonTapped)

This expectation is exhaustive.

It will fail if the analytics is expected and not received. And it will fail if you receive analytics that you did not expect.


You can add ComposableAnalytics to your project by adding https://github.com/oliverfoggin/swift-composable-analytics into the SPM packages for your project.


  • Swift Tools 5.8.0
View More Packages from this Author


Last updated: Mon May 29 2023 23:00:58 GMT-0500 (GMT-05:00)