Conditional view modifiers

When working with SwiftUI views, sometimes we would like to apply different modifiers based on conditions/states:

var body: some view {
  myView
    // if X .padding(8)
    // if Y .background(Color.blue)
}

For most cases, SwiftUI offers inert modifiers, where we can pass a different argument value based on the condition:

var body: some view {
  myView
    .padding(X ? 8 : 0)
    .background(Y ? Color.blue : Color.clear)
}

As covered at WWDC21's session Demystify SwiftUI, this is the recommended way, and we should strive to use this approach as much as possible.

However, there are other modifiers, when applying styles for example, where this solution doesn't work:
in this article, let's explore how we can take care of such cases.

if view extension

The most common solution is to define a new if View extension:

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

This function will apply transform to our view when condition is true. Otherwise, it will leave the original view untouched.

Going back to our style example, this is one way to use it:

var body: some view {
  myView
    .if(X) { $0.buttonStyle(.bordered) }
    .if(Y) { $0.buttonStyle(.borderedProminent) }
}

If-else view extension

Depending on how compact we want our declarations to be, applying different modifiers based on the condition true/false value would cost us at least two modifiers:

var body: some view {
  myView
    .if(X) { $0.buttonStyle(.bordered) }
    .if(!X) { $0.buttonStyle(.borderedProminent) }
}

This is clear and already succinct, however, if we really want to go all-in with View extensions, we can define a new if overload that lets us modify the else branch as well:

extension View {
  @ViewBuilder
  func `if`<TrueContent: View, FalseContent: View>(
    _ condition: Bool, 
    if ifTransform: (Self) -> TrueContent, 
    else elseTransform: (Self) -> FalseContent
  ) -> some View {
    if condition {
      ifTransform(self)
    } else {
      elseTransform(self)
    }
  }
}

Which will make our example use a single modifier:

var body: some view {
  myView
    .if(X) { $0.buttonStyle(.bordered) } else: { $0.buttonStyle(.borderedProminent) }
}

If-let view extension

Sometimes we want to apply a modifier only when another value is not nil, and then use that value in the modifier itself.

In this case we can define a new View extension that lets us do just that:

extension View {
  @ViewBuilder
  func ifLet<V, Transform: View>(
    _ value: V?, 
    transform: (Self, V) -> Transform
  ) -> some View {
    if let value = value {
      transform(self, value)
    } else {
      self
    }
  }
}

The difference from before is that this new function:

  • takes in an optional generic value V instead of a Bool condition
  • passes this generic value V as a parameter of the transform function

Here's an example where a View applies a foreground color only when optionalColor is set:

var body: some view {
  myView
    .ifLet(optionalStyle) { $0.buttonStyle($1) }
}

A word of warning

While these conditional modifiers enable us to apply different things to our views based on conditions, it is very important to understand what happens when the condition changes at run time.

As the view identity changes between the condition branches, switching between them means:

  • the current view is destroyed, and another one from the other condition branch is created
  • if any view where the conditional modifier is applied holds a state, this state is also destroyed and will be reset to its initial value
  • the complete view body is re-evaluated and the view will need to be re-redrawn
  • no animations

It would be fairly simple for the SwiftUI team to add such extensions in the library. However, third-party developers would probably use them without realizing the consequences, and we'd quickly find ourselves in some performance pitfalls, with only the framework to blame.

Where is it ok to use these conditional modifiers then? Whenever the condition is static.
Continuing with our style modifier example, we might have a template view that sets a different style based on the user flow. The style won't change at run time during the flow, and the only way for that style to change is to start a new, different flow.

Conclusions

SwiftUI declarative APIs make Views definition a breeze. When our views need to apply different modifiers based on certain conditions, we should try to use the inert modifier that SwiftUI offers as much as possible.

If we can't use those, we can define and use our conditional view modifiers, letting us keep the same declarativeness that we're accustomed to.

Again, these conditional modifiers work great when the condition value is static. However, they come with a high cost if we need to change them at run time: if you find yourself in this situation, please file a feedback with your use case.

Thank you for reading, and stay tuned for more SwiftUI articles!

⭑⭑⭑⭑⭑

Explore SwiftUI

Browse all