lukecsmith.co.uk

Understanding some View in SwiftUI

Luke, 5 Aug 2024

The first thing you're presented with in a new SwiftUI View is this:

struct MyNewView: View {
    var body: some View {
    ...

.. and this article aims to explain exactly why the body property returns some View like this.

The key to understanding this, is to first understand: View here is not a type. Its a protocol. So the first thing this line says is: something will be returned here that conforms to the View protocol. Once you understand that, you appreciate: you cant return View. You need a type, and View isnt one. Something that conforms to View will be returned - it could be an Image, Text, Circle etc- but it will be a definite type, and it will need to be resolved. We are going to have to say exactly what thing conforming to View will be returned.

The View protocol only has one requirement- that this body property returns this way. The return type is called an opaque return type, and it was created specifically for SwiftUI, in the Swift release that came six months before the release of SwiftUI itself. What is being declared in this return type, is that the compiler will know exactly what is being returned, at runtime, based on the inputs of the view and the logic within the body property.

The logic within the body property can allow for different types to be returned, depending on various inputs, for example: It could return a Text type when a bool is true, and an Image type when that bool is false. The compiler will understand in the moment of running, exactly what real underlying type is being returned here.

var body: some View {
    if showImage == true {
        return Image("dog")
    } else {
        return Text("dog")
    }
}

The some View is hiding and replacing, for convenience, a more complex looking thing: a ConditionalContent generic: _ConditionalContent<Type, Type>.

Heres what this actually looks like without using the opaque return type some View :

var body: _ConditionalContent<Image, Text> {
    if showImage == true {
        return Image("dog")
    } else {
        return Text("dog")
    }
}

As Views become more complex, it would become really cumbersome to have to write out and maintain all of these ConditionalContent definitions (throughout the view too), including updating the types within the angle brackets. So the View protocol, and some View, take away all the need for that. This is the essence of SwiftUI: its visually simple and uncluttered- as convenient as possible for the coder.

For SwiftUI to work well, the identity of each View in the hierarchy needs to be clear. SwiftUI is based on the use of structs for views. UIKit uses classes, and because classes are reference types, there is a reference for their location in memory always available. Its simple for the compiler to be able to know which view is which to perform animations for example - because it can simply query the memory reference and see that its still the same view. With SwiftUI, each redraw of the view frame means scrapping the old struct location in memory and creating a new one somewhere else. Theres no reference to link the old one to the new one.

Two ways were created to allow SwiftUI to identify a view across redraws: explicit identity, or structural identity. Explicit identity is when a view has an ID or something explicitly identifying it. But views are not required to have this, so structural identity is also used, and in effect, this is when a View like the above clearly returns a particular view type, given a certain logic scenario.

In the above example, SwiftUI can know that the Dog image is the same dog image, when the logic is in the same state. If instead, AnyView (wrapper) was applied to the return type, it would no longer be clear to the compiler exactly what the underlying image was. AnyView is a type erasing wrapper type. So SwiftUI can no longer know whether its dealing with the Image of the dog or the Text "dog". So any animations that might want to be run would not appear to work succesfully, or would produce the expected results.

So opaque return types are created to allow the logic to dictate the type that will come back, in a way that will make it easy for SwiftUI to identify the view and treat it consistently.

Ive heard opaque return types described as 'reverse generics' and it makes the most sense to me. With generics, you specify that a type 'blank' will be populated later. For example, this function takes an array where the contents are described as 'Numeric' but the actual type is not yet known.

func count<T: Numeric>(numbers: [T]) {

When this function is called, the actual type will be resolved by the caller, eg :

count([1, 2, 3])

Here the type has been resolved to Ints.

With opaque return types, again the actual returned type has been resolved. But this time, its not the caller of the body property that has resolved the Type. The body property has done the resolving itself. Its reversed the usual role where the caller resolves the type. And why 'opaque' - well its intended to convey that although its not 100% clear what the type is, actually the type is known, but logic dictates it.

Tagged with: