SwiftUI Field Guidebeta

The Layout Protocol

The Layout protocol is very useful when you want to switch between different layouts without recreating views. It allows the views to maintain their identity as you switch layouts, and it lets you animate between different layouts.

Code
AnyLayout(HStackLayout()) {    Color.orange    Capsule()        .fill(Color.yellow)    Text("Hello")}.padding()
View
HStackVStackZStack
200
200

It's also possible to create your own layouts. The Layout protocol lets you implement your own layout algorithm. The two essential methods to implement are sizeThatFits(proposal:subviews:cache:) and placeSubviews(in:proposal:subviews:cache:). The first method is used to compute the size of the entire layout, and the second method let you set the position of the individual subviews.

Building a Flow Layout

One of the things missing in vanilla SwiftUI is a default flow layout. Here is a very basic implementation with configurable spacing. Note that, as with any other layout, the flow layout only becomes as large as needed (it doesn't necessarily fill up the available space).

Code
ScrollView {    AnyLayout(FlowLayout(spacing: 
)) {
/* All of the names */ } /* .border(Color.red) */ .padding(
)
}
View
HStackVStackZStackFlow
200
200

To build this layout, we start by creating a standalone method that lays out views. This layout method does not need to know anything about the actual views except for their sizes. It iterates over the views and places them on the current line. If the view does not fit on the current line, it starts a new line. The function returns both the positions of the views and the size of the entire layout.

Code
 func layout(sizes: [CGSize],             spacing: CGFloat = 8,             containerWidth: CGFloat) ->     (offsets: [CGPoint], size: CGSize) {     var result: [CGPoint] = []
var currentPosition: CGPoint = .zero
var lineHeight: CGFloat = 0
var maxX: CGFloat = 0
for size in sizes {
if currentPosition.x + size.width
> containerWidth { currentPosition.x = 0 currentPosition.y += lineHeight + spacing lineHeight = 0 } result.append(currentPosition) currentPosition.x += size.width
maxX = max(maxX, currentPosition.x)
currentPosition.x += spacing lineHeight = max(lineHeight, size.height) } return (result, .init(width: maxX, height: currentPosition.y + lineHeight)) }

The FlowLayout struct can conform to the layout protocol by implementing the two required methods. To do so, it first gathers the ideal sizes of all the subviews. The layout function directly returns the size needed.

Code
 struct FlowLayout: Layout {     var spacing: CGFloat = 8      func sizeThatFits(proposal: ProposedViewSize,                       subviews: Subviews,                       cache: inout ()) -> CGSize {
let containerWidth =
proposal.width ?? .infinity let sizes = subviews.map { $0.sizeThatFits(.unspecified) } return layout(sizes: sizes, spacing: spacing, containerWidth: containerWidth).size } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { } }

When placing the subviews, we can use the offsets returned from the layout function to place each subview. We make sure to place it within bounds by adding the minX and minY values, respectively.

Code
struct FlowLayout: Layout {    var spacing: CGFloat = 8    func sizeThatFits(proposal: ProposedViewSize,                      subviews: Subviews,                      cache: inout ()) -> CGSize {            }        func placeSubviews(in bounds: CGRect,                       proposal: ProposedViewSize,                       subviews: Subviews,                       cache: inout ()) {        let sizes =            subviews.map {                $0.sizeThatFits(.unspecified)            }        let offsets =            layout(sizes: sizes,                   spacing: spacing,                   containerWidth: bounds.width).offsets        for (offset, subview) in zip(offsets,                                     subviews) {            subview.place(at: .init(x: offset.x                                        +                                        bounds.minX,                                    y: offset.y                                        +                                        bounds.minY),                          proposal: .unspecified)        }    }}

There are a number of ways in which we can improve the layout above. For example, we could have separate values for horizontal and vertical spacing. We could control the vertical alignment within each line, as well as the horizontal alignment of multiple lines.

We recorded a number of Swift Talk episodes about flow layouts, episode 308 and episode 350 are the most relevant.