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

When does the order of SwiftUI modifiers matter, and why?

Answered on 28 Oct 2020
Discover page available: SwiftUI

One of the major differences between SwiftUI and Apple’s previous UI frameworks is how views are configured. Rather than directly modifying various properties, we can instead apply modifiers to the views that we declare, which in turn assign the styles and behaviors that we’re looking to add.

Sometimes, the order in which we apply such modifiers doesn’t matter at all. For example, here we’re creating a specialized label for showing that a certain item has been verified — which uses the built-in Label view introduced in iOS 14 and macOS Big Sur, along with three modifiers that apply our desired styles to the label’s background and text:

struct VerifiedLabel: View {
    var body: some View {
        Label("Verified", systemImage: "checkmark.seal.fill")
            .background(Color.green)
            .foregroundColor(.white)
            .font(.callout)
    }
}

Since none of the modifiers that we use above affect the layout or position of our view, but rather just modify its own internal appearance and the environment in which it will be rendered, we can change the order of them as much as we want, and we’ll still get the exact same result.

However, if we’re instead applying a modifier that wraps a given view, for example in order to modify its layout, then the order of our modifiers can end up having a quite big impact on our final UI.

As an example, let’s now say that we wanted to add a bit of padding to our VerifiedLabel, which can be done like this:

struct VerifiedLabel: View {
    var body: some View {
        Label("Verified", systemImage: "checkmark.seal.fill")
            .background(Color.green)
            .foregroundColor(.white)
            .font(.callout)
            .padding()
    }
}

The result of the above code (which you can see by using the PREVIEW button) might initially be quite surprising. It doesn’t look like our view has any padding at all! To explain what’s going on here, let’s take a quick look at what our view hierarchy actually ends up looking like when constructed — which can be done using the type(of:) function, like this:

let label = VerifiedLabel()
print(type(of: label.body))

Intuitively, we might expect the above expression to simply print the name of our own view type — VerifiedLabel — but here’s what actually ends up getting printed:

ModifiedContent<
    ModifiedContent<
        ModifiedContent<
            ModifiedContent<
                Label<Text, Image>,
                _BackgroundModifier<Color>
            >,
            _EnvironmentKeyWritingModifier<
                Optional<Color>
            >
        >,
        _EnvironmentKeyWritingModifier<
            Optional<Font>
        >
    >,
    _PaddingLayout
>

What the above shows is that each time that we apply a modifier, we actually end up creating a whole new view (at least most of the time), which in turn explains why the padding that we added is seemingly missing when rendering our VerifiedLabel.

It turns out that the padding is indeed there, it’s just that because we’re applying it after applying our background color, we just can’t see it, since the background color will only apply to our label itself — not to the wrapping padding view that the padding modifier adds. That becomes even more clear if we also add a border after adding our padding, which results in a gap between that border and our label:

struct VerifiedLabel: View {
    var body: some View {
        Label("Verified", systemImage: "checkmark.seal.fill")
            .background(Color.green)
            .foregroundColor(.white)
            .font(.callout)
            .padding()
            .border(Color.red)
    }
}

To now actually fix the issue and get the kind of padding that we originally intended, all that we have to do is to move our background modifier so that it’s applied after our padding has been added — like this:

struct VerifiedLabel: View {
    var body: some View {
        Label("Verified", systemImage: "checkmark.seal.fill")
            .foregroundColor(.white)
            .font(.callout)
            .padding()
            .background(Color.green)
    }
}

The same thing applies to other kinds of wrapping modifiers as well, for example cornerRadius, which wraps a given view using a clipping mask in order to give it rounded corners:

struct VerifiedLabel: View {
    var body: some View {
        Label("Verified", systemImage: "checkmark.seal.fill")
            .foregroundColor(.white)
            .font(.callout)
            .padding()
            .background(Color.green)
            .cornerRadius(10)
    }
}

Another case in which the order of our modifiers can be really important is when using ones that are specific to a given type. For example, the bold modifier can only be applied directly to Text values, meaning that if we wanted to change our VerifiedLabel to simply have bold text instead of having an icon, we would need to apply that modifier directly to our Text itself:

struct VerifiedLabel: View {
    var body: some View {
        Text("Verified")
            .bold()
            .foregroundColor(.white)
            .font(.callout)
            .padding()
            .background(Color.green)
            .cornerRadius(10)
    }
}

However, since the first couple of modifiers (foregroundColor and font) only modify our text’s environment, we can also apply our bold modifier after them as well, as long as we keep getting Text values back from those calls. If we, on the other hand, attempted to apply the bold modifier after adding our view’s padding, then we’d get a compiler error saying that our view doesn’t have such a method, since we’re no longer dealing with a Text instance directly:

struct VerifiedLabel: View {
    var body: some View {
        Text("Verified")
            .foregroundColor(.white)
            .font(.callout)
            .padding()
            .bold()
            .background(Color.green)
            .cornerRadius(10)
    }
}

So, to sum up, here are a few things that can be good to keep in mind when working with SwiftUI modifiers:

I hope that you found this answer useful. Feel free to reach out via either Twitter or email if you have any questions or feedback.