Conditionally apply modifier in SwiftUI

@bzamayo - that's exactly what I was after. Thank you.

Also - thanks to everyone for chiming in. It's great to get your insights.

I think is brilliant, and it worked like a charm for me.
However, it crashes the preview. The preview seems not to like " if ". I replaced if with "conditionalModifier" and the preview works too.

Everyone's input has been great, this is what I ended up with atm:

extension View {
    
    /// Applies a modifier to a view conditionally.
    ///
    /// - Parameters:
    ///   - condition: The condition to determine if the content should be applied.
    ///   - content: The modifier to apply to the view.
    /// - Returns: The modified view.
    @ViewBuilder func modifier<T: View>(
        if condition: @autoclosure () -> Bool,
        then content: (Self) -> T
    ) -> some View {
        if condition() {
            content(self)
        } else {
            self
        }
    }
    
    /// Applies a modifier to a view conditionally.
    ///
    /// - Parameters:
    ///   - condition: The condition to determine the content to be applied.
    ///   - trueContent: The modifier to apply to the view if the condition passes.
    ///   - falseContent: The modifier to apply to the view if the condition fails.
    /// - Returns: The modified view.
    @ViewBuilder func modifier<TrueContent: View, FalseContent: View>(
        if condition: @autoclosure () -> Bool,
        then trueContent: (Self) -> TrueContent,
        else falseContent: (Self) -> FalseContent
    ) -> some View {
        if condition() {
            trueContent(self)
        } else {
            falseContent(self)
        }
    }
}

You can use it like this:

someView
    .navigationBarTitle(Text("My title"))
    .modifier(if: model == nil) { $0.redacted(reason: .placeholder ) }
    .modifier(if: model != nil) { $0.unredacted() }
    .modifier(if: model == nil) {
        $0.redacted(reason: .placeholder )
    } else: {
        $0.unredacted()
    }

It would be convenient to add something like this in SwiftUI natively.

1 Like

Saw this recently: Conditional view modifiers

2 Likes

You probably want condition to be plain Bool. It generally is not effectful, doesn't collect caller context, and you use it every time. There's no reason to make it autoclosure.

1 Like

I wanted to use the .if syntax, but @Mini-Stef is right that it breaks the preview even though it compiles. Actually the multiple trailing closures in my example also broke the preview but still compiled.

Remove the auto-closure. It doesn't make any sense to use it because the condition will always be evaluated. As of now, using if for the method name works in the preview. Apple must have fixed that issue. Here's what I have:

 @ViewBuilder func `if`<TrueContent: View>(
    _ condition: Bool,
    then trueContent: (Self) -> TrueContent
) -> some View {
    if condition {
        trueContent(self)
    } else {
        self
    }
}

@ViewBuilder func `if`<TrueContent: View, FalseContent: View>(
    _ condition: Bool,
    then trueContent: (Self) -> TrueContent,
    else falseContent: (Self) -> FalseContent
) -> some View {
    if condition {
        trueContent(self)
    } else {
        falseContent(self)
    }
}
3 Likes

Thx @Peter-Schorn for the note, makes sense since the view renders are immutable and static so it will always be evaluated as you mention.

Btw I've been having a lot of fun with this conditional modifier and leveraged it for other flavours:

Conditional redacted:

/// A modifier that adds a redaction to this view hierarchy if the condition is met, otherwise it is `unredacted`.
private struct ConditionalRedacted: ViewModifier {
    let condition: Bool
    
    func body(content: Content) -> some View {
        content.modifier(if: condition) {
            $0.redacted(reason: .placeholder)
        } else: {
            $0.unredacted()
        }
    }
}

public extension View {
    
    /// Adds a redaction to this view hierarchy if the condition is met, otherwise it is `unredacted`.
    ///
    /// - Parameter condition: The condition to determine if the content should be applied.
    /// - Returns: The modified view.
    func redacted(if condition: Bool) -> some View {
        modifier(ConditionalRedacted(condition: condition))
    }
}

Conditional progress:

/// A modifier that adds a progress view over this view if the condition is met, otherwise it no progress view is shown.
private struct ConditionalProgress: ViewModifier {
    let condition: Bool
    
    func body(content: Content) -> some View {
        content.modifier(if: condition) { content in
            ZStack {
                content
                ProgressView()
            }
        } else: { content in
            content
        }
    }
}

public extension View {
    
    /// Adds a progress view over this view if the condition is met, otherwise it no progress view is shown.
    ///
    /// - Parameter condition: The condition to determine if the content should be applied.
    /// - Returns: The modified view.
    func progress(if condition: Bool, tint: Color? = nil) -> some View {
        modifier(ConditionalProgress(condition: condition, tint: tint))
    }
}

Then use like:

someView
    .navigationBarTitle(Text("My title"))
    .redacted(if: model == nil)
    .progress(if: status == .requested)
1 Like

Another useful one for if-let style:

public extension View {
    /// Applies a modifier to a view if an optional item can be unwrapped.
    ///
    ///     someView
    ///         .modifier(let: model) {
    ///             $0.background(BackgroundView(model.bg))
    ///         }
    ///
    /// - Parameters:
    ///   - condition: The optional item to determine if the content should be applied.
    ///   - content: The modifier and unwrapped item to apply to the view.
    /// - Returns: The modified view.
    @ViewBuilder func modifier<T: View, Item>(
        `let` item: Item?,
        then content: (Self, Item) -> T
    ) -> some View {
        if let item = item {
            content(self, item)
        } else {
            self
        }
    }
}
3 Likes

This is one of the biggest shortcomings in SwiftUI currently. Because we can't easily conditionally apply view modifiers, they should have all been written (by convention) with a conditional argument.

For example, right now I'm trying to conditionally show Save & Cancel buttons in the navigation bar when the user is in an editing mode. I can't find a good example on how to do this, but I sure want to conditionally apply .toolbar.

1 Like

Like this?

2 Likes

Another relevant read with some caveats, and this particularly interesting quote:

In the Rectangle().frame(...) example, we made the view modifier conditional by providing a nil value for the width. This is something that almost every view modifier support. For example, you can add a conditional foreground color by using an optional color, you can add conditional padding by using either 0 or a value, and so on.

5 Likes

While there are some valid uses of this modifier, it is heavily discouraged because flipping the condition causes your view to get destroyed and a new one created in its place.

Demystify SwiftUI goes into specifics of how this mechanism works and how to avoid the pitfalls.

1 Like

I think this conditional modifier usage is why I ran into the problems described here.

1 Like