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

Statically computed default property values

Published on 25 Nov 2020

Establishing a strong set of defaults for a given API can often make it much easier to use, since doing so lets each call site remain as simple as possible, while still enabling customization when needed.

For example, let’s say that we’re working on a UIKit-based view that uses a DateFormatter to display a given date, and that we’d like it to use the current Date and a specific set of formatting styles by default (while still enabling custom values to be assigned as well). Using default property values, we can make that happen like this:

class DateView: UIView {
    var date = Date()
    var formatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .none
        return formatter
    }()
    
    ...
}

However, given that it takes a few lines of code to create our default DateFormatter instance, we might not want to do that right within our list of property declarations — as that could end up making our code quite hard to read (especially if we start adding more properties that follow the same pattern).

One way to solve that issue would be to make our formatter property lazy, and to then define a private factory method that creates its default value. That would let our property declarations remain “clutter-free” — like this:

class DateView: UIView {
    var date = Date()
    lazy var formatter = makeFormatter()
    
    ...
}

private extension DateView {
    func makeFormatter() -> DateFormatter {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .none
        return formatter
    }
}

However, while the above technique works really well for classes, what if we were using a struct instead? For example, if the above DateView was a SwiftUI view, rather than a UIView subclass, then using a lazy property wouldn’t work, since accessing such a property performs a mutation — meaning that we wouldn’t be able to use our formatter within our view’s body:

struct DateView: View {
    var date = Date()
    lazy var formatter = makeFormatter()

    var body: some View {
        VStack {
            // Error: Cannot use mutating getter on immutable
            // value: 'self' is immutable
            Text(formatter.string(from: date))
            ...
        }
    }
}

private extension DateView {
    func makeFormatter() -> DateFormatter {
        ...
    }
}

The reason we get the above compiler error when our view is implemented as a struct is because structs have value semantics. To learn more about that, check out the Basics article “Value and Reference Types”.

At this point, it might seem like we either need to go back to the inline closure-based computation that we originally used (since that doesn’t require our property to be lazy), or to use something like a singleton. But it turns out that there is another way, and that’s to use a private static method to compute our property’s default value — like this:

struct DateView: View {
    var date = Date()
    var formatter = makeFormatter()

    var body: some View {
        ...
    }
}

private extension DateView {
    static func makeFormatter() -> DateFormatter {
        ...
    }
}

The fact that we can use any static API (even private ones) to compute a property’s default value also gives us the opportunity to use a single, statically shared DateFormatter for all of our DateView instances — which in this case could improve the performance of our UI, since we’d no longer recreate a new date formatter every time that our view was updated:

struct DateView: View {
    var date = Date()
    var formatter = defaultFormatter

    var body: some View {
        ...
    }
}

private extension DateView {
    static let defaultFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .none
        return formatter
    }()
}

So an important difference between a lazy property and a non-lazy one is that when computing a lazy property’s default value, we’re within that instance’s own context, while when computing the default value for a non-lazy property we’re in a static context.

Of course, whether we should share a single static instance or create multiple ones will vary depending on each situation. My general recommendation is to only share instances that are either immutable, or shared privately within a single type, as to not introduce global mutable state.

Support Swift by Sundell by checking out this sponsor:

Architecting SwiftUI apps with MVC and MVVM

Architecting SwiftUI apps with MVC and MVVM: Although you can create an app simply by throwing some code together, without best practices and a robust architecture, you’ll soon end up with unmanageable spaghetti code. Learn how to create solid and maintainable apps with fewer bugs using this free guide.