Weekly Swift articles, podcasts and tips by John Sundell.

Writing small utility functions in Swift

Published on 15 Mar 2020

Sometimes, it might seem like the only way to truly impact the overall state of a code base is to make big, sweeping changes — such as changing its architecture, modifying the way data and actions flow through the system, or by adopting new frameworks.

However, while such bigger changes can sometimes be important to make, there’s also often a ton of value to be found in simply addressing more minor pain points, and by making it slightly smoother to iterate on our code during our day-to-day work.

This week, let’s take a look at one way of doing just that — by writing small utility functions that make common tasks easier to perform, and preferred patterns simpler to adopt.

Configuration closures

A fair amount of the code within any given project is likely going to be dedicated to configuring certain objects for use — especially when constructing views using object-oriented UI frameworks, such as UIKit. Like we took a look at in “Encapsulating configuration code in Swift”, finding neat ways to isolate such code can really improve the overall clarity of our actual logic, and this is an area in which utility functions can become especially useful.

As an example, let’s say that we’re currently using the quite popular pattern of structuring the setup of each view’s various subviews using self-executing closures — like this:

class HeaderView: UIView {
    let imageView: UIImageView = {
        let view = UIImageView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.contentMode = .scaleAspectFit
        view.image = .placeholder
        return view
    }()

    let label: UILabel = {
        let view = UILabel()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.numberOfLines = 2
        view.font = .preferredFont(forTextStyle: .headline)
        return view
    }()
    
    ...
}

There’s nothing really wrong with the above implementation, but it would be great if we could reduce the amount of code associated with each property — as each line that we add “pushes” our type’s actual implementation and logic further down. While there are a number of different approaches we could take here, such as using factory methods instead of self-executing closures — let’s see if we could write a simple utility function to help us make the above code a bit more compact, while still using the same basic pattern.

Let’s start by introducing a function called configure, which will take a value that we wish to configure, and a closure in which we can encapsulate all of our configuration code. We’ll also mark that closure’s parameter with the inout keyword to enable our new function to easily be used with value types:

func configure<T>(
    _ value: T,
    using closure: (inout T) throws -> Void
) rethrows -> T {
    var value = value
    try closure(&value)
    return value
}

With the above in place, we can now go back to our HeaderView, and make its subview configuration code read a lot nicer:

class HeaderView: UIView {
    let imageView = configure(UIImageView()) {
        $0.translatesAutoresizingMaskIntoConstraints = false
        $0.contentMode = .scaleAspectFit
        $0.image = .placeholder
    }

    let label = configure(UILabel()) {
        $0.translatesAutoresizingMaskIntoConstraints = false
        $0.numberOfLines = 2
        $0.font = .preferredFont(forTextStyle: .headline)
    }
    
    ...
}

Gone are the local variables and the need to return each view after it’s been configured — leading to more compact code that’s not any harder to read. In fact, it could even be argued that the above code is easier to read, since we’re now calling a function that’s explicitly named “configure” — rather than relying on a convention, like we did before when using self-executing closures.

While there are a number of other approaches that we could’ve taken in order to achieve a similar result, one advantage of our new configure function is that it’s completely generic, and can be used with any type — whether it’s a UIView subclass, any other type of object, or even value types.

For example, here we’re using the exact same pattern as above when setting up a URLRequest value to be used to sync data when the user is on a WiFi-based connection:

struct SyncNetworkTask {
    var request = configure(URLRequest(url: .syncEndpoint)) {
        $0.httpMethod = "PATCH"
        $0.addValue("application/json",
            forHTTPHeaderField: "Content-Type"
        )
        $0.allowsCellularAccess = false
    }
    
    ...
}

The beauty of utility functions in general is that they in many cases enable us to unify various code patterns and styles across each code base — by making the right way to perform a certain task (or at least the way that’s preferred within the project) also become the easiest way.

The power of rethrowing functions

One detail of the above configure function that can be easy to miss is the fact that it’s marked with the rethrows keyword. What that keyword does is that it tells the Swift compiler to only treat that function as throwing if the closure passed to it also throws.

That’s incredibly useful, as it both enables non-throwing use cases like the ones above (without having to use the try keyword in those cases), while also giving us the freedom to throw errors within our configuration closures if needed:

let webView = try configure(WKWebView()) {
    let html = try loadBundledHTML()
    try $0.loadHTMLString(html, baseURL: nil)
    ...
}

Whenever we’re designing any API that accepts synchronous closures, it’s definitely worth considering marking our functions with rethrows — as doing so enables us to throw errors when needed, without making our non-throwing call sites any more complicated.

Reducing boilerplate

Besides letting us codify conventions, utility functions can also often help us avoid common mistakes, and enable us to reduce boilerplate — even when it comes to more specific tasks, such as defining layouts and computing colors.

Especially when using Auto Layout in code, there’s a number of things that we always have to keep in mind — such as remembering to tell each view not to translate its auto resizing mask into constraints (at least in most cases), and to activate each layout constraint that we define — like this:

let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)

NSLayoutConstraint.activate([
    label.topAnchor.constraint(equalTo: view.topAnchor),
    label.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    ...
])

Whenever we have to manually remember to write certain kinds of setup code over and over again, that’s typically a great candidate for a utility function. When it comes to the above example in particular, we’re not looking to introduce a brand new UI abstraction, or to completely change the way we write layout code — our only goal is to make defining constraints slightly easier and more robust.

For example, we might opt for something as simple as a UIView extension that automatically prepares a given view to be used with Auto Layout, and then activates an array of constraints:

extension UIView {
    func layout(using constraints: [NSLayoutConstraint]) {
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate(constraints)
    }
}

With just that tiny extension in place, we can actually make our original code read quite a bit nicer — now looking like this:

let label = UILabel()
view.addSubview(label)

label.layout(using: [
    label.topAnchor.constraint(equalTo: view.topAnchor),
    label.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    ...
])

That may seem like quite a minor change, but the nice thing about utility functions is that they don’t need to make an enormous impact on our code to be useful — simply removing a few sources of boilerplate and potential mistakes is typically enough for them to be worth the effort.

Along those same lines, let’s take a look at another example in which we aim to make it as easy as possible to define dynamic colors that automatically adapt to whether the user’s device is currently running in either light mode or dark mode. The official iOS API for doing just that (apart from defining colors within asset catalogs) looks like this:

let backgroundColor = UIColor { traitCollection in
    switch traitCollection.userInterfaceStyle {
    case .dark:
        return UIColor(white: 0.15, alpha: 1)
    case .light, .unspecified:
        return UIColor(white: 0.85, alpha: 1)
    }
}

That’s a quite nice API, but always having to write a closure containing a switch statement every time we want to define a dynamic color can quickly get tiresome — so let’s again see if we can reduce some of the above verbosity using a utility function.

Since we’re essentially looking to define pairs of colors to use in either light or dark mode, let’s name our new function colorPair, and have it accept one UIColor to use for light mode, and another one for dark mode. We’ll then call the official UIColor API and return the appropriate color for each UIUserInterfaceStyle — like this:

func colorPair(light: UIColor, dark: UIColor) -> UIColor {
    UIColor { traitCollection -> UIColor in
        switch traitCollection.userInterfaceStyle {
        case .dark:
            return dark
        case .light, .unspecified:
            return light
        }
    }
}

Worth noting is that the above function requires us to always create both of our colors at once, rather than only creating the one that’s needed for the current mode. However, that’s something that we could address using @autoclosure if needed.

With the above in place, we can reduce the amount of code needed to define each color pair quite drastically — making our above backgroundColor declaration now look like this:

let backgroundColor = colorPair(
    light: UIColor(white: 0.85, alpha: 1),
    dark: UIColor(white: 0.15, alpha: 1)
)

Combine the above with another quick utility function that lets us define any gray scale UIColor using dot-syntax, and we’ve got ourselves a really neat way of defining dynamic colors — again without any huge abstraction or reinvention of the way colors work on iOS:

extension UIColor {
    static func grayScale(_ white: CGFloat,
                          alpha: CGFloat = 1) -> UIColor {
        UIColor(white: white, alpha: alpha)
    }
}

let backgroundColor = colorPair(
    light: .grayScale(0.85),
    dark: .grayScale(0.15)
)

While writing utility functions such as the ones above, we might start to ask ourselves questions like “Why aren’t the default APIs designed this way?”. Like with most things in programming, designing great APIs is often about balancing a certain set of trade-offs.

While the standard APIs need to optimize both for flexibility, and for a certain degree of consistency with the rest of the platform’s SDK — our own utility functions are free to use a more lightweight, specific design — while still complementing the platforms’s native APIs, rather than aiming to replace them.

That way, we can use our new utility functions whenever possible, while easily be able to fall back to the standard platform APIs whenever we need more power or customization options.

Conclusion

Utility functions can help us codify our conventions, reduce boilerplate, and make common tasks easier to perform. By constantly looking for small ways that we can make writing code smoother and more enjoyable, we’ll keep making our work gradually easier over time — even though each change might seem relatively minor in isolation.

Of course, we shouldn’t go too far, and wrap every single task within some form of convenience API. The process of writing truly great utility functions is, in my opinion, really about identifying common bottlenecks and pain points, and then addressing those — through lightweight APIs that complement the system frameworks, rather than aiming to replace them or completely hide them behind an abstraction.

What do you think? What are some of your favorite utility functions, and have you tried using some variant of the examples shown in this article already? Let me know — along with your questions, comments and feedback — either via Twitter or email.

Thanks for reading! 🚀