loading...

Understanding SwiftUI: View

mtsrodrigues profile image Mateus Rodrigues ・5 min read

You'll hear a lot of "Everything is a View" when diving into the brand new SwiftUI world and, in fact, practically everything we deal with it is a View, even things you wouldn't expect as a Color. So, what is a View? What's so special about it?

Protocol

A View is actually a quite simple protocol with only one required body property whose type also implements the View protocol, and that's it.

protocol View {
    associatedtype Body: View
    var body: Self.Body { get }
}

The body declares the content and behaviour of a View and that's where we code what we want to be display when it's rendered.

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

Opaque Type

The some keyword in the body property is very important. It's a feature called Opaque Type and it's designed to hide a concrete type that implements a given protocol. It's actually a sort of compile magic and the same code can be translated to the following one:

struct ContentView: View {
    var body: Text {
        Text("Hello, World!")
    }
}

Opaque types really shine when dealing with mores complex views. Because of the compositional nature of SwiftUI, the complexity of the body type scales very quickly, but it goes unnoticed thanks to the opaque type.

struct ContentView: View {
    var body: some View {
        HStack {
            Rectangle().fill(Color.green)
            Rectangle().fill(Color.white)
            Rectangle().fill(Color.orange)
        }
        .frame(width: 300, height: 300)
        .border(Color.black)
    }
}

Swift requires an explicit type for computed properties an without this ability to hide concrete types, it would be virtually impossible to write SwiftUI code in a practical way.

struct ContentView: View {
    var body: ModifiedContent<ModifiedContent<HStack<TupleView<(_ShapeView<Rectangle, Color>, _ShapeView<Rectangle, Color>, _ShapeView<Rectangle, Color>)>>, _FrameLayout>, _OverlayModifier<_ShapeView<_StrokedShape<_Inset>, Color>>> {
        HStack {
            Rectangle().fill(Color.green)
            Rectangle().fill(Color.white)
            Rectangle().fill(Color.orange)
        }
        .frame(width: 300, height: 300)
        .border(Color.black)
    }
}

Associated Type

This problem that opaque types solve raises the question of why to use an associated type in the first place. SwiftUI engineers could have chosen a slightly different and simple approach for the View protocol, defining the body property as a simple View.

protocol View {
    var body: View { get }
}

This approach would allow declaring the body type as View, without the need of opaque type, regardless of its complexity.

struct ContentView: View {
    var body: View {
        HStack {
            Rectangle().fill(Color.green)
            Rectangle().fill(Color.white)
            Rectangle().fill(Color.orange)
        }
        .frame(width: 300, height: 300)
        .border(Color.black)
    }
}

The downside of this approach is that SwiftUI relies on static types to optimize the updates. Consider the following example.

struct ContentView: View {

    @State var b: Bool

    var body: some View {
        VStack {
            Text("...")
            Rectangle()
                .fill(b ? Color.red : Color.green)
        }
    }
}

The body property has a concrete type, hidden by the opaque type. You can check this type by accessing the associated type Body.

print(ContentView.Body.self)
// VStack<TupleView<(Text, _ShapeView<Rectangle, Color>)>>

Swift's type system ensures that the type is the same every time the body is computed. This makes it possible to check changes on an individual basis and update only what is needed.

Giving up the concrete type and using only protocol would mean losing the static type and allowing to return anything as long as it conforms to View. The consequence of this is that it would be necessary to redraw the entire screen with each change or perform a much more complicated operation to detect changes in order to update only what is necessary.

You might think that a body with a static type reduces flexibility but SwiftUI provides ways to define optional or conditional views while keeping it statically typed.

struct ContentView: View {

    @State var b1: Bool
    @State var b2: Bool

    var body: some View {
        VStack {
            if b1 {
                Rectangle()
            }
            if b2 {
                Text("...")
            } else {
                Image("...")
            }
        }
    }
}

Although the result on the screen is variable, its type will always be the same and SwiftUI knows the possible outcomes.

print(ContentView.Body.self)
// VStack<TupleView<(Optional<Rectangle>, _ConditionalContent<Text, Image>)>>

The Infinity Recursion Problem

Any implementation of the View protocol will have the same basic structure.

struct ContentView: View {
    var body: some View {
        ...
    }
}

If you think about it, whatever View returned from the body property has its own body, which is also a View and therefore has a body itself and then we end up with an apparently infinity recursion problem.

+--------+   body   +--------+   body   +--------+
|  View  | +------> |  View  | +------> |  View  | . . .
+--------+          +--------+          +--------+

There must be some way to stop this. There is indeed and most SwiftUI built-in View implementations, such as Text, have a special way of stopping this recursion. How? By literally returning Never from the body!

"Never is a proposition that an event doesn’t occur at any time in the past or future. It’s logical impossibility with a time axis; nothingness stretching out in all directions, forever."
(Mattt Thompson)

Never is a so-called uninhabited type, which means that it has no values and can't be constructed. It's mostly used in Swift functions that stop execution such as fatalError(_:file:line:) and its implementation is as simple as possible.

enum Never { }

Extending Never to the implement the View protocol makes it possible to use this type to define a View whose body should never return anything.

extension Never: View {
    typealias Body = Never
    var body: Never { fatalError() }
}

For most of the basic types defined in SwiftUI there is no point in looking at the body property because the type itself and its other properties provide enough information. Text, for example, has a property to store the content to be displayed and that is all the framework needs to know. For types like that, it makes sense that nothing is returned from the body.

extension Text: View {
    public typealias Body = Swift.Never
}

Finally, whenever SwiftUI run into a View with a Never body, it knows when to stop, breaking the recursion in a very simple and clever way.

struct ContentView: View {
    var body: some Text {
        Text("Hello, World!")
    }
}

+---------------+   body   +--------+   body   +---------+
|  ContentView  | +------> |  Text  | +------> |  Never  |
+---------------+          +--------+          +---------+

Text body property hasn't a public access level, but if you try the following code you will get an error.

ContentView().body.body
// Fatal error: body() should not be called on Text

This makes it clear that SwiftUI doesn't actually try to access the body property in cases like this and instead it just type check it.

Conclusion

Being widely used in SwiftUI as it is, View would be expected to be something complicated but as you can see it is actually very simple. It shows how protocols can be powerful and it is a remarkable example of protocol-oriented programming.

Recommended Readings

Understanding Opaque Return Types in Swift
Static Types in SwiftUI
Never

Discussion

pic
Editor guide
 

Amazing post, thanks for sharing!
In fact, these concepts are not familiar to all iOS developers, at first they are very confusing, but in the end they make more sense.
And please, keep sharing!