Swift has a lot to offer, but it isn't always obvious how to get the most out of the language. I often see developers compromise the APIs they build by using stringly typed code while Swift provides features that make stringly typed code unnecessary. In this series, I show you how a handful of simple patterns and techniques can help you build APIs that are type-safe, elegant, and intuitive to use.

A Type-Safe Analytics Library

Integrating analytics into a project is without a doubt my least favorite task. It often compromises the carefully crafted architecture of the project to some extent. What I want to focus on in this series is how we can improve analytics by avoiding string literals and stringly typed code. That is the example we will be using throughout this series.

Let's take a look at the starter project of this series. The project is a notes application. The application isn't functional, though. It simply displays three buttons the user can tap to take an action, that is, create, update, or delete a note. Each action triggers an analytics event. That is a common concept.

A Type-Safe Analytics Library

Let's take a look at the analytics code. Each button is connected to an action of the view controller. The view controller notifies its view model and it is the view model that is responsible for analytics.

Open NotesViewModel.swift and navigate to the createNote(_:) method. This might look familiar. There are a few problems I would like to address in this series. First, the event name is a string literal and so are the property names and values. This isn't uncommon and you might argue that this is an acceptable solution. Second, we invoke the trackEvent(with:properties:) method of a GoogleAnalyticsClient singleton. As you may know, I'm not a fan of singletons. We implement a better, more testable solution in this series.

import Foundation

internal final class NotesViewModel {

    // MARK: - Public API

    func createNote(_ note: Note) {
        GoogleAnalyticsClient.shared.trackEvent(
            with: "create-note",
            properties: [
                "source": "home",
                "kind": "template"
            ]
        )
    }

    func updateNote(_ note: Note) {
        GoogleAnalyticsClient.shared.trackEvent(
            with: "update-note",
            properties: [
                "source": "home",
                "kind": "template",
                "word_count": note.wordCount
            ]
        )
    }

    func deleteNote(_ note: Note) {
        GoogleAnalyticsClient.shared.trackEvent(
            with: "delete-note",
            properties: [
                "kind": "template",
                "word_count": note.wordCount
            ]
        )
    }

}

While these are the most obvious problems, there are several other more subtle problems we address in this series. It is easy to introduce a bug by making a typo and code duplication is another downside of this approach. Because we use string literals for the event names, property names, and property values, we are forced to use the naming conventions of the people that defined the events and properties. That is something I talk about later in this series.

The most important problem in my view is that the API is undefined. Where are the event and property names defined? They are most likely defined in an external document and that is a problem. What are the possible values for the kind and source properties? A developer can pass any string as the value of these properties and that is another problem.

Testability

It is challenging and time-consuming to test analytics because it usually requires an integration test. While that is true, there are some aspects we can easily unit test. We can make sure events and properties are properly formatted before they are sent to an analytics service. That is only possible if you design and implement analytics in such a way that it is testable. That is another aspect we look at in this series.

Designing an API

This series is about more than avoiding string literals and stringly typed code in a project. It is about designing APIs that are intuitive, elegant and difficult to misuse. Proper API design isn't something that is talked about often, but it's so important if you write code that is used by others, whether those others are team members, third parties, or the open source community.

You can avoid so many problems if you take some time to design your API and consider how that API is going to be used and how it could be misused. This includes defining constraints, carefully applying access control, and putting yourself in the shoes of the consumer of the API.

Let's take a look at how this seemingly simple API can be misused. The GoogleAnalyticsClient class defines the trackEvent(with:properties:) method to send an event to Google Analytics. The method defines two parameters, event of type String and a dictionary of properties. The keys of the properties dictionary are of type String and the values are of type Any. As the name suggests, Any means you can pass in an object of any type. What happens if you accidentally pass an enum as the value of a property? It is unlikely the analytics client supports that since it wouldn't know what to do with it. In the best case, the analytics client ignores the property. In the worst case, your application crashes.

One of the goals of any API should be to avoid issues like that, which is possible by carefully considering how the API will be used and make it difficult and non-trivial to misuse. That is one of the goals for the API we create in this series.

Protocol-Oriented Approach

The notes view model accesses the analytics client through a static property on the GoogleAnalyticsClient class. The static property returns a singleton that is accessed throughout the project. That is a common pattern, but it compromises the testability of the project. In this series, we use a protocol-oriented approach to make unit testing a breeze. I show you a few techniques you can choose from.

What's Next?

Swift offers everything you need to design and implement type-safe, elegant, and intuitive to use APIs. You may argue that the use of string literals and stringly typed code is sometimes easier and faster, but I don't agree with that. You need to consider the bigger picture. You can only evaluate a solution if you also consider how easy it is to learn and adopt an API, how often the API results in bugs, and what the cost is of maintaining the API. These aspects are often overlooked or simply ignored.