Tim Roesner's Blog

Understanding SwiftUI's View type

With the introduction of Swift 5.1 and SwiftUI, new opaque return types were added to the language. Probably the most popular usage of this is the some View type, which is used for the body of SwiftUI View structs. In this article I’ll cover the common pitfalls that we encounter as a result of this opaque type, where it can be use, and how we can get around its limitations.

Properties

Most common throughout SwiftUI is the body property which makes use of this new some View type:

var body: some View {
	VStack {
		// Your layout
	}
}

This works without issues, as it is a computed property and contains only a single sub-type. But if you want to use this with a stored property, you run into the following issue:

// Property declares an opaque return type, but has no 
// initializer expression from which to infer an underlying type
var cell: some View

Since View is just a protocol to which many types adhere, one might think that we could just use it as our type here. However does not work as expected, as it has associated type requirements:

// Protocol 'View' can only be used as a generic constraint 
// because it has Self or associated type requirements
var cell: View

In order to solve the issue of type inference we either need to initialize it right then and there, or we can use a generic approach to infer the type at a later point:

struct SectionView<CellContent: View> {
	var cell: CellContent
}

Even though we can substitute any type that conforms to the View protocol for CellContent, once we do we are bound to that specific type. As illustrated in this code sample:

var section = SectionView(cell: Text("Lane"))
// Cannot assign value of type 'Image' to type 'Text'
section.cell = Image("firefly")

The same applies when we have more than one property of the CellContent type within our SectionView, since the compiler expects them to be of the same type. However we can define more than one generic View type:

struct Cell<Content, Accessory> where Content: View, Accessory: View {
	var content: Content
	var accessory: Accessory
}

let cell = Cell(content: Text("San Francisco"), accessory: Image(systemName: "cloud.fog"))

Functions

Just like computed properties we can use the some View type as a return type:

func cellContent(for item: Item) -> some View {
	VStack {
		// Your layout
	}
}

This works just like before, as it contains only a single sub-type, in this case the VStack, which conforms to the View protocol. However if we add any control statements we run into issues again:

// Function declares an opaque return type, but the return 
// statements in its body do not have matching underlying types
func cellContent(for item: Item) -> some View {
	if horizontalSizeClass == .compact {
		return VStack { ... }
	} else {
		return HStack { ... }
	}
}

We can solve this issue with the @ViewBuilder parameter attribute. As of Xcode 12.0 this attribute is automatically added to the body property of all SwiftUI View structs.

@ViewBuilder
func cellContent(for item: Item) -> some View {
	if horizontalSizeClass == .compact {
		VStack { ... }
	} else {
		HStack { ... }
	}
}

Note: In order for the ViewBuilder to work as intended we may not include return.

Parameters

Similar to the stored property that we looked at before, parameters have similar limitations:

// 'some' types are only implemented for the declared type 
// of properties and subscripts and the return type of functions
func cell(with content: some View) -> Cell

// Protocol 'View' can only be used as a generic constraint 
// because it has Self or associated type requirements
func cell(with content: View) -> Cell

Just like before we can add a generic type, but this time we only need to apply it to the function:

func cell<Content: View>(with content: Content) -> Cell

As SwiftUI is heavily influenced by the closure syntax, we often have closure parameters that then produce an output of the View type. One could assume that since it’s the return type of the closure we should be able to use some View? However like before we get an error:

// 'some' types are only implemented for the declared type 
// of properties and subscripts and the return type of functions
func cell(content: @escaping () -> some View) -> Cell

To solve this we can add the generic type to the function, and can also apply the @ViewBuilder attribute if the content should support control statements:

func cell<Content: View>(@ViewBuilder content: @escaping () -> Content) -> Cell

The same syntax can also be used for parameters within initializers.

Summary

Opaque return types are a great tool to get around limitations of the associated type requirements of the View protocol. However we just saw, they come with their own limitations. Fortunately Swift already provides us with great tools to combat these.

Next Post:

next-post

DiffableDataSource with CellRegistration for iPad Sidebar