Articles, podcasts and news about Swift development, by John Sundell.

200 weeks of Swift

Published on 13 Dec 2020

If I had to name just one thing that I believe to be an overarching theme among the 200 weekly articles that I’ve now written about Swift development, I’d say that it’s the notion that making solid technical decisions is all about researching and then carefully comparing the tradeoffs of each potential solution.

When writing these articles, I’ve always aimed to be informative, rather than convincing. To share my learnings from adopting a given pattern, technology or framework, without necessarily telling you — the reader — that you should adopt them the exact same way that I did. Because at the end of the day, what solutions that will be the best fit for each given project will always depend on a mix between requirements, tradeoffs, and the personal opinions of the people involved in building it.

So, for my 200th and final* weekly article, I thought I’d sum up what I consider to be three of my major overall learnings after close to four years of continuous writing about Swift, its features, and many of the patterns that it’s commonly used with.

*Although this is indeed the final article in the weekly series that I’ve now been doing for 200 weeks, I’ll of course keep publishing many other kinds of articles on this website going forward. Check out this article for more info about my new publishing format.

The value of value types

When I first started programming in Swift, I strongly associated value types with data models. That is, I’d make my models, configuration types, options and other kinds of data representations all structs and enums, while keeping the rest of my code strictly object-oriented through classes and protocols.

However, these days, I tend to decide between value and reference types based purely on whether I really need a given type to hold any kind of shared mutable state. If I don’t (which tends to be the case), then I go for a value type, which not only lets me take advantage of all of the powerful language features that Swift’s value types offer — but doing so also encourages me to keep my various types as immutable as possible, even as I keep iterating on their logic.

For example, with my old way of thinking, if I were to write something like a simple Dataloader that acts as a wrapper around Foundation’s URLSession API — then I’d make that a class, mostly because it “felt right” for instances of such a type to be actual objects that are passed by reference, rather than values:

class DataLoader {
    typealias Handler = (Result<Data, Error>) -> Void

    private let urlSession: URLSession

    init(urlSession: URLSession = .shared) {
        self.urlSession = urlSession
    }

    func loadData(from url: URL,
                  then handler: @escaping Handler) {
        let task = urlSession.dataTask(with: url) {
            data, response, error in
            ...
        }

        task.resume()
    }
}

However, there’s really nothing about the above type that warrants reference semantics. It doesn’t hold any form of state that needs to be shared across multiple call sites, and for this kind of low-level code, I’d actually prefer things to stay that way.

So, by turning my DataLoader into a struct instead, I’ll both get a strong protection against accidental mutations (as a struct can only mutate itself within methods that are explicitly marked as mutating), and I’ll also be able to remove my manually implemented initializer, since the compiler will now generate a memberwise one (like it does for all structs):

struct DataLoader {
    typealias Handler = (Result<Data, Error>) -> Void

    var urlSession = URLSession.shared

    func loadData(from url: URL,
                  then handler: @escaping Handler) {
        let task = urlSession.dataTask(with: url) {
            data, response, error in
            ...
        }

        task.resume()
    }
}

One tradeoff with the above solution, however, is that the underlying URLSession instance that my DataLoader is using is now exposed as part of its API. While that could be problematic in certain cases, I’d gladly accept that tradeoff in this case — especially since this particular DataLoader type is simply a wrapper around URLSession to begin with.

Since DataLoader is now a value type, that also means that it can be mutated using value semantics, which in turn will make those mutations local by default. To illustrate, let’s say that we wanted to add support for attaching a series of HTTP headers to each request that a given DataLoader will make. Because we’re now dealing with a value type, adding those headers either requires us to create a brand new instance, or to copy an existing one — which is great — since that prevents us from accidentally sharing those header values across the entire application:

struct DataLoader {
    typealias Handler = (Result<Data, Error>) -> Void

    var urlSession = URLSession.shared
    var headers = [String : String]()

    func loadData(from url: URL,
                  then handler: @escaping Handler) {
        var request = URLRequest(url: url)
request.allHTTPHeaderFields = headers

        let task = urlSession.dataTask(with: request) {
            data, response, error in
            ...
        }

        task.resume()
    }
}

So while I still use classes in many types of situations, especially when I really need to share a piece of state among multiple types, structs have gradually become my default tool for implementing new types — especially when working with very value-centric frameworks, such as SwiftUI.

Using types as documentation

One of my favorite aspects of Swift is its very strong, static type system. Increasingly, it feels like as long as my code compiles, it’ll actually do what I intended, which is quite amazing. But strong typing is not only great for compile time verification, it can also be a fantastic tool for communication.

Take Foundation’s URL type as an example. At the end of the day, URLs are just strings, but there’s also a certain set of (quite complex) rules that they have to follow. So while every URL is a string, not every string is a URL. To me, that’s exactly the kind of data that warrants its own, dedicated type — even when we’re dealing with custom, app-specific data that could technically be represented using raw values, such as strings or integers.

For example, the following UserSession type contains two properties that are currently stored as raw String values:

struct UserSession {
    var id: String
    var accessToken: String
    ...
}

While both of the above two properties might be quite self-explanatory when viewed in the context of their enclosing UserSession type, chances are quite high that we’d also like to pass those values around individually — at which point we’d now be dealing with completely context-less strings.

Compare that to the following UserSession implementation, which now uses strong types that clearly explain what each piece of data does, and — as an added bonus — will also make any code using those values much more type safe:

struct UserSession {
    var id: User.ID
    var accessToken: AccessToken
    ...
}

At first, it might seem like implementing such wrapper types will be a lot of work — but both thanks to Swift’s aforementioned value type features, and to the many built-in protocols that the Swift standard library ships with — it could simply be done like this:

// Marking a type as Identifiable gives us a nested ID type
// based on its 'id' property (UUID in this case):
struct User: Codable, Identifiable {
    let id: UUID
    var name: String
    ...
}

// When using RawRepresentable, each value will be automatically
// encoded and decoded using its raw value (String in this case):
struct AccessToken: Codable, RawRepresentable {
    var rawValue: String
}

With the above in place, we’ll now be able to freely pass each of our UserSession values around individually without losing their contextual meaning. For example, here’s how we might extend our DataLoader from before with an API for authorizing its requests using a given access token — and since that value now has a dedicated type, our resulting code becomes quite self-documenting:

extension DataLoader {
    mutating func authorize(with token: AccessToken) {
        headers["Authorization"] = "Bearer \(token.rawValue)"
    }
}

These types of changes might seem quite minor in the grand scheme of things, but in my experience, they can really have a quite big impact on the overall semantics and readability of a given system. Because after all, code is only written once, but read multiple times, regardless if we work on our own or with other people, so optimizing for readability and ease of understanding is almost always worth the effort.

Protocols are not always the answer

In the early days of Swift, the term “protocol-oriented programming” became incredibly popular, and many Swift developers (myself included) started to view protocols as the default way of defining all kinds of abstractions. However, as is the problem with most patterns that are called something like “X-oriented” or “X-driven”, it’s easy to get the impression that the goal is to follow the pattern itself as much as possible, which can most often be quite counter-productive.

So while I still consider protocols to be an incredibly powerful tool, the way that I personally use them has certainly changed over the past 200 weeks. Now, I look at protocols as just another tool in my toolbox, rather than as a design goal. For example, protocols are still a really great option in situations such as:

On the other hand, if I’m only looking to model a single requirement, then I’d often rather use a single function instead. When doing that, I don’t need to maintain two separate declarations, while still achieving a high degree of separation of concerns and testability.

For example, let’s now say that we’re looking to build a UserLoader type that’ll use our DataLoader from before as its underlying networking engine. Rather than creating a protocol that has a 1:1 relationship to that concrete DataLoader type, let’s instead model the functionality that we’re looking to use — the loadData(from:then:) method in this case — as a function that looks like this:

typealias DataLoading = (URL, @escaping DataLoader.Handler) -> Void

Then, rather than injecting a DataLoader instance directly into our new UserLoader, we’ll instead be able to inject any function that matches the above signature, and our user loader can then simply call that function when loading its data:

struct UserLoader {
    typealias Handler = (Result<User, Error>) -> Void

    var dataLoading: DataLoading
    var decoder = JSONDecoder()

    func loadUser(withID id: User.ID,
                  then handler: @escaping Handler) {
        let url = resolveURL(forID: id)

        dataLoading(url) { result in
            do {
                let data = try result.get()
                let user = try decoder.decode(User.self, from: data)
                handler(.success(user))
            } catch {
                handler(.failure(error))
            }
        }
    }
}

Thanks to the fact that Swift supports first class functions, we can then pass a reference to the DataLoader method that we’re looking to use simply by doing this:

let dataLoader = DataLoader()
let userLoader = UserLoader(dataLoading: dataLoader.loadData)

So what’s the benefit of doing the above, and — sticking to that overall theme again — what are the tradeoffs? Perhaps my favorite thing about the above pattern is that it makes testing so much easier.

Even though protocols can heavily improve the testability of a given type, they always require us to implement dedicated mock types for each piece of functionality that we’re looking to simulate within our tests. When using the functional approach, however, mocking a dependency becomes as simple as using a closure — for example like this:

class UserLoaderTests: XCTestCase {
    func testSuccessfullyLoadingUser() throws {
        let user = User(id: UUID(), name: "John Appleseed")
        let data = try JSONEncoder().encode(user)

        let loader = UserLoader { _, handler in
    handler(.success(data))
}

        var result: Result<User, Error>?
        loader.loadUser(withID: user.id) { result = $0 }
        XCTAssertEqual(try result?.get(), user)
    }
}

To learn more about the above approach, check out “Mock-free unit tests in Swift”.

One of the major tradeoffs, though, is that the above technique doesn’t scale very well for types that have a larger number of dependencies, as we’d have to inject each of those dependencies as separate functions — which can quickly get quite messy. Also, when we need to call multiple methods on a single type, using a protocol (or injecting an instance of that type directly) is most likely going to be easier than passing multiple separate functions.

Even so, it’s a great technique to keep in mind, along with a few other protocol alternatives, rather than immediately reaching for a protocol in all kinds of situations.

Conclusion

When I started writing these weekly articles, Swift by Sundell didn’t yet exist, Swift 3 was the new cool version of the language, and I had no idea whether anyone would actually want to read what I was about to write.

Now, 200 weeks later, not only have I learned so much about Swift, what it means to be a writer, and all of the things that are involved in running a website like this — I’ve also had the true pleasure of discussing these topics with so many of you — both on Twitter, through email, and at various conferences and meetups around the world.

So whether this was the first weekly article that you’ve ever read, or your 200th one, thank you. All of your combined support, feedback and encouragement is truly what has kept my going during these 200 weeks of continuous writing. Because as long as the Swift community remains interested in what I have to write, I’ll do my very best to keep writing.

Thanks for reading! 🚀