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

Conditional compilation within Swift expressions

Published on 03 Sep 2021

New in Swift 5.5: It’s now possible to conditionally compile postfix member expressions using Swift’s #if compiler directive. Let’s take a look at what kinds of situations that this new feature could be really useful in.

Working around platform differences

Although many of the built-in APIs and frameworks work exactly the same way across Apple’s platforms, there are certain differences that we might need to work around. For example, when using SwiftUI to build an app that runs on both iOS and the Mac, we might find ourselves in the following type of situation — in which we’re getting an error when attempting to apply the iOS-specific InsetGroupedListStyle to a List:

struct ItemList: View {
    var items: [Item]

    var body: some View {
        List {
            ...
        }
        // Error: 'InsetGroupedListStyle' is unavailable in macOS
        .listStyle(InsetGroupedListStyle())
    }
}

In general, these kinds of issues can be worked around using a compile-time platform check — but before Swift 5.5, we’d have to first break our List out into a separate expression, and then apply different listStyle modifiers separately using an #if-based operating system condition:

struct ItemList: View {
    var items: [Item]

    var body: some View {
        let list = List {
            ...
        }

        #if os(iOS)
        list.listStyle(InsetGroupedListStyle())
        #else
        list.listStyle(InsetListStyle())
        #endif
    }
}

In isolation, the above code doesn’t look that bad, but if our ItemList view were to gain additional subviews (or if we’d need to perform additional compile-time checks within it), then its body could quickly become quite hard to read.

So perhaps a more robust solution to this problem would be to instead extract the above platform check into a dedicated modifier method — for example like this:

extension View {
    func defaultListStyle() -> some View {
        #if os(iOS)
        listStyle(InsetGroupedListStyle())
        #else
        listStyle(InsetListStyle())
        #endif
    }
}

With the above in place, we can simply apply our new defaultListStyle modifier within our ItemList view, and we no longer have to deal with any platform differences when constructing our actual UI:

struct ItemList: View {
    var items: [Item]

    var body: some View {
        List {
            ...
        }
        .defaultListStyle()
    }
}

However, while the above is certainly a neat technique when working with modifiers and styles that we’re looking to reuse multiple times across a project, always having to define a dedicated method each time we encounter a platform difference can become quite tedious.

This is where Swift 5.5’s new support for #if conditions within postfix member expressions comes in.

Inline checks within expressions

When using Swift 5.5, we now have the option to inline #if directives right within our expressions. So, going back to our ItemList, we can now conditionally apply each of our listStyle modifiers completely inline — without first having to break our expression up into multiple parts:

struct ItemList: View {
    var items: [Item]

    var body: some View {
        List {
            ...
        }
        #if os(iOS)
        .listStyle(.insetGrouped)
        #else
        .listStyle(.inset)
        #endif
    }
}

Above we’re also making use of another new language feature that enables us to refer to SwiftUI list styles (and other kinds of protocol-based types) using dot syntax. Check out “Using static protocol APIs to create conforming instances” to learn more about that.

Nice! Of course, this new capability also works for other kinds of #if-based compile-time checks — including using the standard DEBUG flag to check if our app is being compiled using its debug build configuration, and any custom compiler flags that we might’ve defined.

As an example, here’s how we could use a custom flag to conditionally visualize the final rendering size of one of our views:

struct DynamicIcon: View {
    var name: String

    var body: some View {
        Image(systemName: name)
            .resizable()
            .aspectRatio(contentMode: .fit)
            #if SHOW_ICON_SIZES
            .overlay(GeometryReader { geo in
                Text("\(Int(geo.size.width)) x \(Int(geo.size.height))")
                    .font(.footnote)
                    .background(Color.blue)
                    .foregroundColor(.white)
            })
            #endif
    }
}

To learn more about how to define and use custom compiler flags, check out “Using compiler directives in Swift”.

Conclusion

So, how should we pick between these new inline #if conditions versus creating dedicated modifiers for working around platform differences and for performing other kinds of compile-time checks? While every developer will certainly have their own preferences — for me, it all depends on whether a given pattern will be repeated within a code base, or whether it’s something that we’re only performing once.

For repeated compile-time checks, I still prefer to create dedicated functions, since that lets me wrap those checks up into a much simpler API, but when working around minor platform differences (like we did above), I prefer the new inline style. It’s already proving to be quite useful when working on cross-platform SwiftUI-based projects.

If you have any questions, comments, or feedback, then feel free to reach out via email.

Thanks for reading!