Conditional ViewModifiers in SwiftUI

Brian Masse
3 min readDec 29, 2023

The declarative ViewModifier system in SwiftUI is great— it allows designers to quickly build up and work with complex Views— however it lacks a clear solution when dealing with conditional modifiers.

You can follow along with this gitHub repo. In this example we have a toggle that when active should cause the Text to display one way, and when inactive cause it display another way.

struct ContentView: View {
@State var toggle: Bool = false

@ViewBuilder
private func makeToggle() -> some View {
Text( "toggle" )
.padding()
.background( toggle ? .red : .gray.opacity(0.5) )
.cornerRadius(20)
.onTapGesture { withAnimation {
toggle.toggle()
} }
}

// MARK: Body
var body: some View {
makeToggle()
.padding(.bottom)

HStack {
Text("hello World!")
Image(systemName: "globe.americas")
}
.padding(7)
}
}

Method 1.

If we wanted the text to have the same style with different values—ie. changing the color on toggle—this is easy enough to do with in-line conditions.

.foregroundStyle( toggle ? .blue : .red)

However this cannot handle more complex conditions, where on toggle you have one viewModifier, and on untoggled you have another.

Method 2.

The native solution is to abstract the view you are applying the modifier to into its own unit, then place it in a more traditional condition statement

@ViewBuilder
private func makeText() -> some View {
HStack {
Text("hello World!")
Image(systemName: "globe.americas")
}
.padding(7)
}

var body: some View {
makeToggle()
.padding(.bottom)

if toggle {
makeText()
.background(.red)
} else {
makeText()
.opacity(0.5)
}
}

In this case its fairly trivial to just wrap the Text in a ViewBuilder and put that in an if statement. However this method has a few problems

  1. Often you don’t want to move something into its own ViewBuilder func or Struct: a lot of the time it just doesnt make sense to separate it from the rest of the code, other times it makes your code harder to read, and sometimes it can just be tedious to abstract it like that.
  2. The condition in the view is bulky. There is still repetition in declaring the makeText() function, and your view hierarchy code is disrupted by the condition. This problem is worse the more complex the condition, or the more outcomes you could have.
  3. Its not always possible to abstract the entire view. Sometimes the conditional modifier has to be in the middle of the other viewModifiers, so you can only tuck the first half of the modifiers into the ViewBuilder, forcing you to repeat the second half outside in your condition statement

For these 3 reasons, I strongly discourage using this solution.

Method 3

Create a custom viewModifier that handles conditions.

extension View {
@ViewBuilder
func `if`<Content: View>( _ condition: Bool, contentBuilder: (Self) -> Content ) -> some View {
if condition {
contentBuilder(self)
} else { self }
}
}

This is a one line, scalable solution to the condition problem:

HStack {
Text("hello World!")
Image(systemName: "globe.americas")
}
.padding(7)
.if(toggle) { view in view.background(.red) }
.if(!toggle) { view in view.opacity(0.5) }

In addition to its declarative simplicity, this solution automatically handles state changes. It also allows for complex conditional ViewModifiers: the contentBuilder arg in the if modifier can receive any view, so you can modify the original view as much as you want in that call.

HStack {
Text("hello World!")
Image(systemName: "globe.americas")
}
.padding(7)
.if(!toggle) { view in
view
.padding(7)
.background(Colors.lightAccentGreen)
.cornerRadius(20)
.bold(true)
.font(.title)
}
.if(toggle) { view in view.opacity(0.5) }
}
.padding(.vertical)

Thank you for checking out this article. If you liked it, please give it a cheers, and feel free to follow me on my other socials:

--

--