Understanding how to work with View as a type

I've been struggling with a problem for a while. I found a solution eventually, but I don't fully understand why it's a solution.

Basically I wanted to write a custom modifier as a View extension method that applies different sets of modifiers depending on its arguments. The trouble is, View is not actually a type, so under most circumstances you can't do something like this:

func conditionalModifier(condition: Bool) -> some View {
    if condition {
        return self.someModifier()
    } else {
        return self.someOtherModifier()
     }
}

It seems that any two expressions that evaluate to types that conform with some View have no type equality or in common with each other that I can use. However, this limitation is overridden in a View's body getter or, as I discovered in the article that solved the problem for me, a method of View annotated with @ViewBuilder.

So, although my immediate problem is solved, I don't really know why/how this works. Why can a View's body or @ViewBuilder have multiple return paths, but other methods can't?

The problem is not really that View is not a type (but you're right, it's a protocol): the problem is that some View means "some specific concrete type that conforms to View", so the caller must be guaranteed that in every single case conditionalModifier is called, always the same concrete type is returned. In order to tell the callers of this function that it will return "any type that conforms to View, possibly a different one each time it's called", the function should return any View.

The solution you found probably compelled you to write something like this:

@ViewBuilder
func conditionalModifier(condition: Bool) -> some View {
    if condition {
        self.someModifier()
    } else {
        self.someOtherModifier()
     }
}

A hint to understand the difference is in the fact that there is no return. In fact, this wouldn't compile:

// this will not compile
@ViewBuilder
func conditionalModifier(condition: Bool) -> some View {
    if condition {
        return self.someModifier()
    } else {
        return self.someOtherModifier()
     }
}

The difference is that the @ViewBuilder, which is a @resultBuilder, is transforming your code in something else; @ViewBuilder is returning always a specific concrete type of View, but we're not aware of it thanks to the opaque some View result type: in this particular case, assuming that someModifier returns a View of type A and someOtherModifier returns a View of type B, the @ViewBuilder is returning a View of type _ConditionalContent<A, B>.

1 Like

This isn't quite what's happening. No methods are allowed to return different underlying types.

@ViewBuilder is a result builder, which takes your if statement and transforms it into a call to a buildEither method. (Put another way, when you are writing code inside a @ViewBuilder method, you're writing in a domain-specific language that's transformed into plain Swift behind the scenes; the if statement that you see doesn't mean the same thing as a "plain" Swift if statement, but rather does whatever the result builder wants it to do.)

In the case of ViewBuilder, the return type of buildEither is _ConditionalContent<TrueContent, FalseContent>. As you can see, this means that the underlying return type is the same regardless of condition.

5 Likes

So Swift annotations can actually change the code in function bodies, not just wrap them? Wow. And applying one to a protocol or member of a protocol means it implicitly applies to implementations (which seems to be the case for View.body)? I had heard something about SwiftUI using a DSL, but not in the tutorials etc I read. The rules in this DSL seem a bit vague, where in some contexts you can include some types of non-View expression and return a View, and some you can't. I can see now why they had to disallow the use of switch, but it does seem possible to use if ... else if ... else ... with more than two branches.

You can use switch in a ViewBuilder. It wasn't in the initial implementation, but it was added soon after. It expands to _ConditionalContent<_ConditionalContent<_>> behind the scenes in a similar fashion to if.

1 Like