SwiftUI Field Guide

Introduction

SwiftUI is a declarative framework for building user interfaces. When coming from UIKit, SwiftUI is very different. There is little knowledge that we can transfer from UIKit to SwiftUI — the layout system is different, the state system is different, animations are different, and so on.

This guide is designed to help understand the layout system in SwiftUI. We hope that the visual examples invite to explore SwiftUI's views and modifiers. The interactive examples do not run actual SwiftUI code, but are reimplemented in a simplified SwiftUI port. As such, they might not always match SwiftUI in a pixel-perfect way. (However, if our port behaves differently, we do consider it a bug.)

View Trees

To understand the SwiftUI layout system, it is essential to understand how the code we write translates into a tree. For example, consider the following code:

Code
Text("Hello, World!")    .padding()    .background { Color.accentColor }
Preview

In the example above, the order of the modifiers is critical. If we switch the order of the background and padding modifiers, the background will only be the size of the text, and the padding is added outside of the background.

The view builder syntax used in SwiftUI is designed to construct view trees. In the example below, we can see how two very different view trees get constructed when the order of the modifiers changes. In one case, the background is applied to the padded text, whereas in the other case, the padding is added to the text with the background.

Code
Text("Hello, World!")    .padding()    .background { Color.accentColor }
Tree

Throughout this guide, we show trees for the views we construct. When we're debugging our own views, it can often be helpful to think about the view tree that gets constructed. Understanding view trees is not only important for layout, but is a necessary skill for understanding the state system, the animation system, the environment and much more.

Proposing and Reporting

The essence of SwiftUI's layout system is very simple: a parent proposes a size to its child, and the child reports a size. In practice, however, the layout system is not always easy to work with. This is because every view in SwiftUI can potentially have a different algorithm for choosing its size based on the proposal.

In the example below, we can see the proposing and reporting in action. At first, a size of 200×200 is proposed to the background modifier. The background modifier then proposes that same size to its primary child, the padding. The padding subtracts 32 from the width and height, and proposes the smaller size to the text. The text reports its intrinsic size, and the size is then reported back up the tree. The padding adds 32 to the width and height, and reports its size to the background. The background then reports its size to the parent. Finally, to render the secondary child of the background, the size of the primary child (the padded text) is proposed to the color.

Code
Text("Hello, World!")    .padding()    .background { Color.accentColor }
Tree

 

1/1

Views have very different behaviors when it comes to proposing and reporting. For example, a regular image view ignores the proposed size but returns its intrinsic size. An aspect ratio modifier can propose twice to its child — once to figure out the underlying aspect ratio and once to actually render the child. A shape such as a rectangle unconditionally accepts the proposed size. Understanding how different views respond to proposed sizes is essential for building complex layouts. There are no requirements on the response: a view can accept, ignore or do something else with the proposed size.

Theory: Attribute Graph

The layout system in SwiftUI is based on a tree of views. These view trees get constructed from the code we write. The layout system then constructs an attribute graph from the view tree. The process of constructing the attribute graph is almost completely opaque. In our book Thinking in SwiftUI, we go over the process of how this works. The WWDC video Explore SwiftUI animation is currently the only public available first-party documentation that explains the attribute graph.

There are a few useful things to know about the attribute graph. It is called the attribute graph because it is based on attribute grammars. In essence, when we view our SwiftUI code as a tree, certain properties get passed down the tree, and other properties get reported back up the tree.

A value that gets passed down the tree is called an inherited attribute. Examples include the proposed size, the environment, the safe area and the current transaction. Properties that are reported back up the tree are called synthesized attributes. Examples include the reported size, alignment guides, layout priorities and preferences. When writing SwiftUI, we find that it can sometimes be helpful to think about whether a property is inherited or synthesized.