Weekly Swift articles, podcasts and tips by John Sundell.

Designing reusable Swift libraries

Published on 31 May 2020
Basics article available: Generics

Code reuse is one of those programming concepts that can be much more complex than what it first might seem like. While it’s easy to argue that code duplication should be avoided at all costs and that implementations should always be reused and shared whenever possible — it’s important to remember that all abstractions that we end up introducing also come with a certain cost.

So writing pragmatic, reusable code often comes down to striking a balance between reducing duplication wherever possible, while also trying to avoid having to introduce too many layers of abstraction or complexity in order to unify our various implementations.

Striking such a balance becomes especially important (and, arguably, difficult) when designing and building reusable libraries — so this week, let’s take a look at a few principles and techniques that can be good to keep in mind when doing just that.

RevenueCat

This ad keeps all of Swift by Sundell free for everyone. If you can, please check this sponsor out, as that directly helps support this site:

RevenueCat

RevenueCat: In-app purchases and subscriptions made easy. RevenueCat makes it simple to build in-app purchases, manage your products and subscribers, and analyze your IAP data – no server code required.

Packaging up a generic concept

Whether we’re working on a single code base, or multiple ones, there are often opportunities for us to share code by extracting narrowly scoped pieces of logic into separate libraries. Apart from the code reuse aspect, doing so also enables us to test and iterate on that logic in isolation — which can be incredibly useful, especially within larger code bases with long overall build times.

When deciding what kind of code to extract into a separate library, it’s usually a good idea to try to pick something that can be implemented as a truly generic concept. That doesn’t necessarily mean that the code itself needs to be completely generic, but rather that the logic itself isn’t tied to any specific feature or domain.

As an example, imagine that we’re working on an app that makes heavy use of tags in order to sort, filter and provide recommendations for various kinds of content. While the way that we actually use those tags within our app might be really specific to our particular domain, the concept of tags itself is something that could definitely be generalized into a reusable library — a TagKit, if you will.

Let’s say that a core part of our app’s tagging logic is implemented using a Tagged protocol, as well as a TaggedCollection that lets us store and retrieve elements based on their tags:

protocol Tagged: Hashable {
    var tags: [String] { get }
}

struct TaggedCollection<Element: Tagged> {
    private var elements = [String : Set<Element>]()

    mutating func add(_ element: Element) {
        for tag in element.tags {
            elements[tag, default: []].insert(element)
        }
    }

    mutating func remove(_ element: Element) {
        for tag in element.tags {
            elements[tag]?.remove(element)
        }
    }

    func elements(taggedWith tag: String) -> Set<Element> {
        elements[tag] ?? []
    }
}

Let’s start the process of creating our TagKit by moving the above protocol and type into a separate library, which we’ll create as a Swift package. Once we’ve set up the package itself, the first step would be to mark all of the APIs that we wish to be part of our library’s public interface as public, so that they’ll be accessible outside of our library module — like this:

public protocol Tagged: Hashable {
    var tags: [String] { get }
}

public struct TaggedCollection<Element: Tagged> {
    private var elements = [String : Set<Element>]()
    
    // Note that we have to explicitly add a public initializer
    // in order to be able to initialize a type outside of
    // the module that it's declared in:
    public init() {}

    public mutating func add(_ element: Element) {
        for tag in element.tags {
            elements[tag, default: []].insert(element)
        }
    }

    public mutating func remove(_ element: Element) {
        for tag in element.tags {
            elements[tag]?.remove(element)
        }
    }

    public func elements(taggedWith tag: String) -> Set<Element> {
        elements[tag] ?? []
    }
}

While the above kind of change is quick and easy to make, a key aspect of library development is to carefully consider what to actually make parts of our public API, versus what to keep internal within the library itself.

As an example, let’s say that we now want to enable instances of our TaggedCollection type to be encoded and decoded using Swift’s built-in Codable API. Since all of our collection’s elements would also need to be Codable in order to make that happen, one approach would be to add that protocol as an additional requirement for conforming to Tagged — which would let us simply mark TaggedCollection as being Codable as well:

public protocol Tagged: Hashable, Codable {
    var tags: [String] { get }
}

public struct TaggedCollection<Element: Tagged>: Codable {
    ...
}

However, here’s where we have to put on our “API designer hat” for a second, and think about whether requiring all Tagged types to also conform to Codable is really a good idea. After all, what does encoding and decoding have to do with having support for tags?

Although making that association might not seem like a big deal, adding more requirements than what’s absolutely needed could make our library less flexible and harder to adopt — especially as we might continue to add more requirements in the future if we choose to follow this design.

Thankfully, there’s another approach that we could take in this case, and that’s to use Swift’s conditional conformances feature — which enables us to make our TaggedCollection conform to a given protocol only when its Element type also does.

Using that — while also moving the requirement of conforming to Hashable into our TaggedCollection itself — lets us simplify our Tagged protocol to now only require a single property, all without sacrificing any functionality:

public protocol Tagged {
    var tags: [String] { get }
}

public struct TaggedCollection<Element: Tagged & Hashable> {
    ...
}

extension TaggedCollection: Codable where Element: Codable {}

With the above change in place, our API now scales really nicely — from simply adopting our Tagged protocol, to using it within TaggedCollection, to being able to encode and decode collection instances — with requirements that are gradually introduced only when needed.

Strong types and escape hatches

Currently, our tagging system uses raw strings to represent each tag, which might’ve been completely fine when that system was implemented specifically for a single app — but since we’re now looking to turn it into a stand-alone library, we might want to make things a bit more strongly typed.

An initial idea on how to achieve that might be to model all of the tags that we’re currently using as an enum, with one case for each tag, and to then update both our Tagged protocol and our TaggedCollection to use that type instead of String values:

public enum Tag: String, Codable {
    case newRelease
    case onSale
    case promoted
    ...
}

public protocol Tagged {
    var tags: [Tag] { get }
}

public struct TaggedCollection<Element: Tagged & Hashable> {
    private var elements = [Tag : Set<Element>]()

    ...

    public func elements(taggedWith tag: Tag) -> Set<Element> {
        ...
    }
}

However, while the above approach might’ve worked great within a single app, it’s quite problematic in the context of a reusable library — since the library itself will need to contain all of the tags that any of the apps using it will ever need, which isn’t very sustainable in the long run.

One way to fix that problem would be to add an ”escape hatch” to the above enum — that is, an API that enables us to implement our own custom tags when needed — for example by introducing a custom case, like this:

public enum Tag: Hashable, Codable {
    case newRelease
    case onSale
    case promoted
    ...
    case custom(String)
}

While the above approach does work, and might even be the right design decision in certain situations, it comes with the downsides that we’d both have to implement our own conversion to and from strings (since our enum is no longer backed by a raw String value), and we’d also have to manually conform to Codable as well.

Instead, let’s take a different approach, and implement our Tag type as a struct — which enables us to keep track of each tag’s underlying String value using a property. We’ll also take this opportunity to enable Tag values to be easily expressed using string literals as well:

public struct Tag: Hashable, Codable {
    public var string: String

    public init(_ string: String) {
        self.string = string
    }
}

extension Tag: ExpressibleByStringLiteral {
    public init(stringLiteral value: String) {
        string = value
    }
}

The great thing about the above approach is that not only are we now free to define app-specific Tag values in whichever way we want — we can still enable the same “enum-like” dot-syntax to be used for our most frequent tags by adding static computed properties for them, for example like this:

public extension Tag {
    static var newRelease: Self { #function }
    static var onSale: Self { #function }
    static var promoted: Self { #function }
    ...
}

The above #function symbols will at compile time automatically expand to the name of their enclosing property, giving us the exact same raw string mapping as enums provide.

With the above in place, we now both get the convenience of being able to easily define our tags using raw strings, while also getting the additional type safety and self-documenting qualities that strong types provide.

The importance of testing the public API

It’s fair to say that a major part of building a solid library is putting enough automated tests in place to ensure that its various functionality and behaviors will keep working as expected as it continues to evolve.

As an example, let’s say that our new tagging library also contains a RecommendationEngine that lets us quickly generate an array of recommendations from a collection of tagged elements. For the sake of simplicity, we’ll use the following implementation — which generates its recommendations by shuffling all of the elements that match a given tag, and then returns the first three matches:

public struct RecommendationEngine<Element: Tagged & Hashable> {
    private let collection: TaggedCollection<Element>

    public init(collection: TaggedCollection<Element>) {
        self.collection = collection
    }

    public func recommendations(forTag tag: Tag) -> [Element] {
        let elements = collection.elements(taggedWith: tag)
        return Array(elements.shuffled().prefix(3))
    }
}

Fun fact: While the above is just an example, it’s actually quite close to how the first version of this website’s recommendation system was implemented. Nothing wrong with using simple algorithms if they end up doing the job.

While the above implementation is indeed simple, it’s actually quite problematic from a testing perspective, since it contains an element of randomness (through its use of shuffled()). One way to address that problem would be to extract that source of randomness into a closure that could then be overridden within our tests — for example like this:

public struct RecommendationEngine<Element: Tagged & Hashable> {
    internal var sorting: (Set<Element>) -> [Element] = { $0.shuffled() }

    private let collection: TaggedCollection<Element>

    public init(collection: TaggedCollection<Element>) {
        self.collection = collection
    }

    public func recommendations(forTag tag: Tag) -> [Element] {
        let elements = collection.elements(taggedWith: tag)
        return Array(sorting(elements).prefix(3))
    }
}

Our new sorting property is marked as internal, since we’re currently considering it an implementation detail of our library, rather than a part of its public API. We could then access that property within our tests using the @testable import command — which gives us access to all of the imported module’s internal APIs, as well as its public ones. That way, we could write tests like this:

@testable import TagKit

class RecommendationEngineTests: XCTestCase {
    func testReturningFirstThreeMatchedElements() {
        let articles = (0..<5).map { index in
            Article(
                title: "Article-\(index)",
                tags: ["tag"]
            )
        }

        var collection = TaggedCollection<Article>()
        articles.forEach { collection.add($0) }

        var engine = RecommendationEngine(collection: collection)

        engine.sorting = { array in
            array.sorted(by: { $0.title < $1.title })
        }

        let recommendations = engine.recommendations(forTag: "tag")
        XCTAssertEqual(recommendations, Array(articles[..<3]))
    }
}

While the above approach works, and is a very common way of writing unit tests for app targets, it’s questionable whether it’s a good approach for testing libraries.

The problem with using internal APIs to write library tests is that those capabilities won’t be available to the actual production code that’ll use our library — which in turn makes it easy to overlook design flaws and APIs that are too limited. After all, if we need a certain API to be able to test our library, chances are that at least one of the apps using it will need that API too.

So let’s turn our new sorting API into a proper public one instead. While we could of course just change that property’s access level to public and leave things like that, let’s tweak it a bit before exposing it as a part of our official API.

Just like how we earlier introduced a dedicated type for representing tags, let’s do the same thing here — and create a Sorting type that’ll act as a thin wrapper around a closure that’ll perform the actually sorting:

public struct Sorting<Element: Hashable> {
    public typealias Body = (Set<Element>) -> [Element]

    public var body: Body

    public init(body: @escaping Body) {
        self.body = body
    }
}

For convenience, let’s also provide a default shuffled implementation of our new Sorting type, using the same static property-based technique that we used earlier:

public extension Sorting {
    static var shuffled: Self {
        .init { $0.shuffled() }
    }
}

With the above in place, let’s now go back to our RecommendationEngine and make it accept an instance of our new Sorting type as part of its initializer. We’ll also take this opportunity to parameterize our maxElementCount as well — further making our public API more customizable and powerful without sacrificing any convenience:

public struct RecommendationEngine<Element: Tagged & Hashable> {
    private let collection: TaggedCollection<Element>
    private let sorting: Sorting<Element>
    private let maxElementCount: Int

    public init(collection: TaggedCollection<Element>,
                sorting: Sorting<Element> = .shuffled,
                maxElementCount: Int = 3) {
        self.collection = collection
        self.sorting = sorting
        self.maxElementCount = maxElementCount
    }

    public func recommendations(forTag tag: Tag) -> [Element] {
        let elements = collection.elements(taggedWith: tag)
        return Array(sorting.body(elements).prefix(maxElementCount))
    }
}

A really neat side-effect of the above change is that we can now keep implementing different Sorting variants — both within our library itself, and externally within our app projects. For example, here’s another implementation which sorts the Set that it’s given based on a key path:

public extension Sorting {
    static func basedOn<V: Comparable>(
        _ keyPath: KeyPath<Element, V>
    ) -> Self {
        .init { set in
            set.sorted {
                $0[keyPath: keyPath] < $1[keyPath: keyPath]
            }
        }
    }
}

With the above pieces in place, we can now drop the @testable prefix from our unit test’s import statement, and write our test using the exact same set of APIs that our production code will have access to — like this:

import TagKit

class RecommendationEngineTests: XCTestCase {
    func testReturningFirstThreeMatchedElements() {
        let articles = (0..<5).map { index in
            Article(
                title: "Article-\(index)",
                tags: ["tag"]
            )
        }

        var collection = TaggedCollection<Article>()
        articles.forEach { collection.add($0) }

        let engine = RecommendationEngine(
            collection: collection,
            sorting: .basedOn(\.title)
        )

        let recommendations = engine.recommendations(forTag: "tag")
        XCTAssertEqual(recommendations, Array(articles[..<3]))
    }
}

In general, using unit tests while developing libraries is a great way to “dog-food” all of our APIs and to work out how to handle various edge cases. Because at the end of the day, if our library is difficult to test, it’ll likely be difficult to use in production as well.

Support Swift by Sundell by checking out this sponsor:

RevenueCat
RevenueCat

RevenueCat: In-app purchases and subscriptions made easy. RevenueCat makes it simple to build in-app purchases, manage your products and subscribers, and analyze your IAP data – no server code required.

Conclusion

While this article didn’t manage to cover every single aspect of library design and development, I hope that it has provided some useful insight into how I approach building stand-alone Swift libraries.

It’s also important to point out that not all reusable components need to be implemented as separate libraries — sometimes simply sharing a piece of logic as an internal class or struct is more than good enough — especially since libraries are also dependencies that need to be managed, updated and maintained. But when warranted, building a completely reusable system as its own library definitely has a ton of benefits.

What do you think? Do you tend to structure parts of your code as reusable libraries, or is it something that you’ll try out? Let me know — along with your questions, comments and feedback — either via Twitter or email.

Thanks for reading! 🚀