Search code examples
swiftswiftuiconditional-statementsimplementationviewbuilder

How is Conditional Content created in SwiftUI?


I was trying to understand the inner workings of _ConditionalContent but I wasn't able to implement a resultBuilder for it. This is how some of the resultBuilders for _ConditionalContent are defined:

static func buildEither<T: View, F: View>(first: T) -> _ConditionalContent<T, F>

This doesn't make sense to me because how is F determined? It can't be type erased to AnyView because I have seen both type parameters be concrete SwiftUI views, eg. _ConditionalContent<Text, Button>. It only makes sense that in conjunction with the other buildEither(second:) function is the final type for _ConditionalContent determined.

I wasn't able to get a rough-cut version of this working so if anyone can explain to me how to implement this using result builders please let me know:

struct ConditionalConent<T: View, F: View> {
    let trueContent: T
    let falseContent: F
}

@resultBuilder
struct ConditionalContentBuilder {
    static func buildBlock<V: View>(_ content: V) -> V {
        return content
    }

    static func buildEither<T: View, F: View>(first: T) -> ConditionalContent<T, F> {
        // ... how is ConditionalContent created?
    }

    static func buildEither<T: View, F: View>(second: F) -> ConditionalContent<T, F> {
        // ... how is ConditionalContent created?
    }
}

Solution

  • Result builders were officially defined by SE-0289. The section Selection statements describes how if/else statements are transformed. For your question, the answer is given by this sentence:

    Note that all of the assignments to vMerged will be type-checked together, which should allow any free generic arguments in the result types of the injections to be unified.

    Normally, in Swift, each statement is type-checked independently of other statements, so the statements inside the two branches of if/else cannot influnce each others' type-checking. But in an if/else statement inside a result builder, where both of the branches assign the result to the same (synthesized) variable, Swift uses both branches together to infer the type of that variable.

    This is similar to how both branches of a ?: operator must have the same type. For example, using your definition of ConditionalContent, we can write this:

    import SwiftUI
    
    struct ConditionalContent<T: View, F: View> {
        var condition: Condition
    
        enum Condition {
            case isTrue(T)
            case isFalse(F)
        }
    }
    
    let vMerged: ConditionalContent = 1 > 2
        ? .init(condition: .isTrue(Text("really big one")))
        : .init(condition: .isFalse(Text("normal sized one").opacity(0.5)))
    
    print(type(of: vMerged))
    // output: ConditionalContent<Text, ModifiedContent<Text, _OpacityEffect>>
    
    

    Swift can infer the types for T and F by looking at both branches of the ?: operator. A result builder performs a similar inference using both branches of an if/else statement.