Weekly Swift articles, podcasts and tips by John Sundell.

What makes code “Swifty”?

Published on 23 Feb 2020

Although programming languages are formally defined by their syntax, the ways in which they get used in practice are arguably just as much determined by their current conventions. After all, syntax-wise, most “C-influenced” languages look incredibly similar — to the point where you could write Swift in ways that almost makes it look like JavaScript, or C#, or C itself.

Within the Swift community, the phrase “Swifty code” is often used to describe code that follows the conventions that are currently the most popular. However, while Swift’s core syntax hasn’t changed that much since its original introduction, its conventions have dramatically shifted over time.

For example, many Swift developers remember the transition from Swift 2 to Swift 3 as a big change in terms of syntax, but the majority of those changes weren’t really syntax changes — they were changes to the standard library’s API based on a new set of naming conventions. Add Swift 4’s introduction of key paths and Codable, Swift 5.1’s function builders, property wrappers and opaque return types, and many more APIs and features that have been introduced over the years — and it’s starting to become quite clear that what makes code “Swifty” is somewhat of a moving target.

This week, let’s take a closer look at Swift’s core set of conventions, to try to come up with an answer to the question of what really does make code “Swifty”?

Aligned goals

In a way, a simple answer to the above question could be “code that’s well-aligned with Swift’s core set of goals”. After all, while Swift’s various APIs, conventions and language features tend to change over time, its fundamental goals mostly remain the same — so if we can write our own code in ways that matches those goals, then we’ll have a much better chance of making our code feel natural and clear within any given Swift context.

So, what exactly are those goals? The about page on Swift’s official website lists three keywords: Safe in terms of minimizing developer mistakes, Fast when it comes to speed of execution, and Expressive, in that Swift aims to be as clear and as easy to understand as possible.

Let’s take a look at a few different things that can be good to keep in mind in order to make our own code follow those principles.

Clarity through strong type safety

Let’s start with the first keyword — safety. The fact that Swift puts a very strong emphasis on type safety is hard to miss — with its static type checks, a powerful generics system, and the need to do things like type erasure in order for the compiler to be able to verify our code’s structure as it’s being compiled.

However, it’s quite common to encounter situations in which it’s less obvious that our code’s type safety could be improved — which could also make it feel more “Swifty”, and in turn, become easier to work with. For example, here we’re storing a collection of notes based on the name of the group that they belong to:

struct NoteCollection {
    var notesByGroup: [String : [Note]]
    ...
}

At first glance, the above code may seem perfectly fine. However, one detail that’s not at all obvious when looking at the declaration above is how we’re dealing with ungrouped values, as well as with a special group containing all of the user’s recently opened notes — which is currently done by either passing an empty string, or "recent", when subscripting our dictionary:

let groupedNotes = collection.notesByGroup["MyGroup"]
let ungroupedNotes = collection.notesByGroup[""]
let recentNotes = collection.notesByGroup["recent"]

While the above design may have a perfectly valid justification (for example, the structure we’ve used might be how our notes are organized when loading them over the network), it does lead to some of our call sites becoming quite cryptic — which in turn increases the chances of developer mistakes. It’s easy to forget that an empty string means that all ungrouped notes should be retrieved, and what happens if the user names one of their custom groups “recent”?

Let’s see if we can make the above code more type safe, and in doing that, make it feel more “Swifty” as well. Since we have three distinct use cases for our notesByGroup dictionary, let’s replace its String-based keys with a custom enum that models those three variants as distinct cases, like this:

enum Group: Hashable {
    case none
    case recent
    case named(String)
}

struct NoteCollection {
    var notesByGroup: [Group : [Note]]
    ...
}

The above may seem like a subtle change, but it makes our call sites a lot more clear, since we’re now leveraging the type system to distinguish between our three separate types of groups — all without making our API any more complex:

let groupedNotes = collection.notesByGroup[.named("MyGroup")]
let ungroupedNotes = collection.notesByGroup[.none]
let recentNotes = collection.notesByGroup[.recent]

That’s perhaps the essence of what makes code “Swifty” in terms of type safety. While there are many ways to make an API really complicated in order to make it more type-safe, the trick is to use Swift’s language features to find a way to add that type safety without making our code harder to understand or use.

While type safety is commonly used to prevent a value of type B from incorrectly being passed to an API that accepts A, strong typing also often provides a way to improve the semantics and logic of our code as well. In the following example, our code is technically type-safe — as we are using Swift’s generics feature to implement a LoadingOperation that can load any Resource that conforms to a Loadable protocol:

class LoadingOperation<Resource: Loadable> {
    private let resource: Resource

    init(resource: Resource) {
        self.resource = resource

        if let preloadable = resource as? Preloadable {
            preloadable.preload()
        }
    }
    
    ...
}

However, the fact that we’re conditionally casting our resource to see if it also conforms to Preloadable (and if so, we preload that resource) is arguably a bit strange. Not only does the above implementation make it hard to understand how to make a resource preload (as the type system doesn’t give us any hints that we’re supposed to conform to Preloadable to make that happen), it’s also quite unintuitive to have preloading be a side-effect of initializing an operation.

Instead, let’s make preloading an explicit API that’s only available when an operation’s Resource conforms to Preloadable — like this:

extension LoadingOperation where Resource: Preloadable {
    func preload() {
        resource.preload()
    }
}

The above change both makes it a lot more clear what the conditions are for a resource to be preloaded, and we can now remove our type casting side-effect from our initializer — big win!

What’s important to note is that writing “Swifty” code from a safety perspective is definitely not about using generics as much as possible. Rather, it’s about using the type system’s various aspects and features selectively to make our code easier to understand and use (and harder to misuse).

The path to performance

The second of Swift’s core goals, to be fast, is something that’s a bit trickier to reason about in general terms. After all, a major part of writing high-performance code comes down to measuring, fine-tuning, and measuring again. However, one way to make our code more aligned with Swift itself in terms of performance is to make full use of what the standard library has to offer — particularly when working with collections, such as strings.

Like we took a look at in “String parsing in Swift” and “Slicing Swift collections”, the Swift standard library is highly optimized for performance, and enables us to perform many common collection operations in a highly efficient manner — given that we use the right APIs, that is.

For example, one common way to remove a certain set of characters from a string is to use the old replacingOccurences(of:with:) API that Swift’s String type inherited from its Objective-C cousin, NSString. Here we’re using a series of calls to that API to sanitize a string by removing a set of special characters:

let sanitizedString = string
    .replacingOccurrences(of: "@", with: "")
    .replacingOccurrences(of: "#", with: "")
    .replacingOccurrences(of: "<", with: "")
    .replacingOccurrences(of: ">", with: "")

The problem with the above implementation is that it’ll cause 4 separate iterations through our string — which might not be a problem when working with shorter strings, or when doing the above within a code path that’s not hit very often, but it could become a bottleneck in situations when we need maximum performance.

Thankfully, Swift often doesn’t require us to pick between performant code and elegant code — all we have to do is to switch to a more appropriate API, one that just makes one pass through our string in order to remove each character that’s contained within a Set, like this:

let charactersToRemove: Set<Character> = ["@", "#", "<", ">"]
string.removeAll(where: charactersToRemove.contains)

So, to make our code more “Swifty” from a performance point of view, sometimes all that we have to do is to explore what the standard library has to offer when faced with a given task — and especially when it comes to collections, chances are quite high that there’s an elegant, simple API that also gives us great performance characteristics available.

Clear, expressive naming

Finally, let’s take a look at the third and last keyword — expressive. While it’s easy to think of expressiveness as something that’s purely cosmetic, and that involves nitpicking method names until they all read as grammatically perfect English sentences, it’s ultimately all about making our code clearly convey its meaning.

Let’s say that we’ve written a function that’s currently called getContent, which loads the data for a bundled Content model, and then decodes it:

func getContent(name: String) -> Content? {
    guard let url = Bundle.main.url(
        forResource: name,
        withExtension: "json"
    ) else {
        return nil
    }

    guard let data = try? Data(contentsOf: url) else {
        return nil
    }

    return try? JSONDecoder().decode(Content.self, from: data)
}

Again, at first glance the above function may seem perfectly fine. There are no obvious bugs, and it gets the job done. However, in terms of being expressive, it could definitely be improved.

First of all, its current name — ”get content” — doesn’t really tell us how the content will be retrieved. Will it simply be created as a new instance, will it be loaded over the network, or something else? Also, the fact that it simply returns nil in case an error occurred could make it harder to debug in case something ever starts to fail — since we won’t get any indication as to what actually went wrong.

So let’s see if we can improve things, by first renaming our function to loadBundledContent (to make it clear that we’re loading the content from our app bundle). We’ll also give it an external parameter label to make it read a bit nicer, and finally, we’ll make it report any error that it encountered by throwing it — like this:

func loadBundledContent(named name: String) throws -> Content {
    guard let url = Bundle.main.url(
        forResource: name,
        withExtension: "json"
    ) else {
        throw Content.Error.missing
    }

    guard let data = try? Data(contentsOf: url) else {
        throw Content.Error.missing
    }

    do {
        return try JSONDecoder().decode(Content.self, from: data)
    } catch {
        throw Content.Error.decodingFailed(error)
    }
}

For more on the above way of designing throwing APIs, check out the very first article on this site — “Providing a unified Swift error API”.

Here’s what the call site looks like before and after our change:

// Before
let content = getContent(name: "Onboarding")

// After
let content = try loadBundledContent(named: "Onboarding")

While it’s important not to get too hung up on what we name our functions and types (it’s often a matter of taste and preference, after all), if we can find ways to more clearly convey what each of our APIs do, then that’s a big win — as it not only makes it easier for new developers to familiarize themselves with our code base, it can also often make our code more pleasant to work with long-term.

Conclusion

In my opinion, writing “Swifty” code is not about using as many language features as possible, or about making our code needlessly complex by deploying Swift’s most advanced features to solve simple problems — it’s about aligning the way we design and express our code and its various APIs with Swift’s core set of principles.

By making our code use Swift’s type system both to ensure correctness and to make its functionality more clear, by fully utilizing the standard library, and by conveying the intent of our code through expressive naming and API design — we often end up with code that better matches Swift itself, and isn’t that what writing “Swifty” code is all about?

Got questions, comments or feedback? I’d love to hear from you! Either find me on Twitter or send me an email.

Thanks for reading! 🚀