Conditionally apply modifier in SwiftUI

Just to be clearer on the motivation here - it's great to learn about tricks that I can use for different modifiers, but it would be neater to be able to handle a bunch of modifiers like so

    Card().padding().if(selected){
        $0.border(Color.red)
        .shadow(radius: 5)
        .gesture(myDragGesture())
    }

rather than individually

    Card().padding()
        .border(Color.red,width: selected ? 4 : 0 )
        .shadow(radius: selected : 5 ? 0 )
        .gesture(myOptionalDragGesture(selected:selected))
    }

(by the way - I can do this with the 'if' function from higher up, though there is some annoying AnyView action going on there)

Essentially, I'm back to generic vs specific here. I want to be able to treat all modifiers in the same way as bits of standard code rather than having a bunch of different special cases. Of course the fact that I want it doesn't have any bearing on reality...

A modifier is really a View that has the modified View as its child, and is thus part of the view hierarchy. SwiftUI intentionally embeds the structure of your view hierarchy into your program’s types for reasons Joe explained in this thread.

1 Like

@mayoff thanks for the pointer.

It's interesting to read about the motivation here (making it easy to diff/animate the view hierarchy)
It's beyond my skill to understand the tradeoffs here, but I'm still left feeling that (for possibly excellent reasons), we're left with an API where the primary constructs (Views) are second class citizens in the language

If View was a struct or a class, then we can pass it around, write logic, functions etc that work with it. It's a standard thing that I can program with.

e.g. there is nothing controversial or hard about writing

func if(condition:Bool,do:(UIView)-> UIView) -> UIView {...}

the equivalent with some View is much more tricky as the compiler doesn't allow

func if(condition:Bool,do:(some View)-> some View) -> some View ){...}

it's interesting to know that a modifier is really a View, but not as helpful as that would be if it were a class

Perhaps I'll get used to this. As I say, I'm fairly new to Swift UI. It's kinda magical to use - both in a bad way (confusing, particular, hard to reason about) and a good way (powerful functionality appears at minimal cost)

Is this what you want? This is similar to @cukr's version but avoids the TupleView wonkiness. It doesn't use AnyView either and I think it is the most efficient form (although I'm no expert on SwiftUI so I don't know if it is superior to the 'modifiers with nulled out parameters' way).

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

View is a Protocol with Associated Type (PAT). PAT is already somewhat of a second class citizen so that’s probably true.

I’d say that ViewBuilder (which is still an experimental feature) makes it much easier to work with.

Still, you can that, writing logic/function like @bzamayo example, or passing concrete type of View around, albeit with slightly more difficulty.

1 Like

Individual Views are all struct values, that you can operate on in the usual ways through generic functions over the View protocol. One of the things about SwiftUI, though, is that it really prefers that you always return the exact same type of view. Early versions of SwiftUI did in fact have effectively a single View struct, and we found that this made the animation system in particular hard to use, because it was too easy to write things like:

condition ? Button().opacity().scale() : Button().scale().opacity()

which seem like they should work, but which are difficult to implement robustly in all cases, since the order of transformations and how they should be individually animated has to be reconstructed for all possible combinations. By basing the design instead on a protocol and generic wrapper structs, SwiftUI is intentionally trying to make the path of least resistance the one that gives the most robust results. The function builder magic is largely unnecessary, and beside the point to some degree; without it, you could still write if as a higher-order function, as @bzamayo noted , and we could construct compound views with regular constructors.

10 Likes

When you say 'we', it raises a question, how much involvement did you and other core members had in the framework besides related language features?

I‘m just curios. :slight_smile:

@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