---
title: "Exploring SwiftUI Layout Protocol | Creating Custom Layout"
slug: "exploring-swiftui-layout-protocol-creating-custom-layout"
date: "2022-08-11T21:00:18.000Z"
description: "Apple introduces new SwiftUI's Layout protocol with the release of iOS 16. It is a powerful tool for constructing custom views with SwiftUI elegance."
---

# Exploring SwiftUI Layout Protocol | Creating Custom Layout

Apple introduces a new SwiftUI `Layout` protocol with the release of iOS 16. It is a powerful tool for constructing custom views with SwiftUI elegance. In this post, I will cover what `Layout` is and how it can be used.

In the end, we will construct a custom table view that auto-arranges its subviews. Complete code is provided!

![](https://alexdremov.me/content/images/2022/08/Screenshot-2022-08-12-at-00.21.52.png)

## Conforming to Layout

The discussed `Layout` is a new protocol that allows you to select a way of arranging your views.

Through it, you literally can say at what coordinates you want to place subviews. For example, now `HStack`, `VStack`, and `ZStack` can easily be implemented through it in iOS 16.

```swift
protocol Layout : Animatable
```

To conform to the protocol, you need to define two methods

```swift
func sizeThatFits(
    proposal: ProposedViewSize,
    subviews: Self.Subviews,
    cache: inout Self.Cache
) -> CGSize


func placeSubviews(
    in bounds: CGRect,
    proposal: ProposedViewSize,
    subviews: Self.Subviews,
    cache: inout Self.Cache
)
```

You also can define `makeCache(subviews:)` if your layout has some calculations that do not depend on a proposal and depend only on subviews. Then, you can make your calculations in `makeCache(subviews:)` and then use these values.

### Method `sizeThatFits`

```swift
func sizeThatFits(
    proposal: ProposedViewSize,
    subviews: Self.Subviews,
    cache: inout Self.Cache
) -> CGSize
```

Returns a size that indicates how much space the container needs to arrange its subviews. SwiftUI can call this method several times, probing your view and finally deciding the best option

💥

Only finite sizes can be returned. Returning size with infinite coordinate **results in a crash without a reasonable call stack**, so keep attention to sizes that you return

To calculate it, you can use passed arguments:

#### proposal

Basically, it's SwiftUI's proposal for your view's size. I like to think about it as a negotiation.

> I can give you this much space. What's your size is going to be? Will you even fit?  
>   
> — SwiftUI negotiator

`ProposedViewSize` is like a `CGSize` that also can have some specific values.

*   The `zero` proposal; the view responds with its minimum size.
*   The `infinity` proposal; the view responds with its maximum size.
*   The `unspecified` proposal; the view responds with its ideal size.

You can also access `width` and `height` of proposal if it is not of the above values.

The proposal can have one dimension fixed and the second one as `nil`. For example, an `HStack` might measure the flexibility of its subviews’ widths, while using a fixed value for the height.

#### subviews

It is just a container of subviews' proxies `LayoutSubview`. Through it, you can ask subviews about their size, and also give them your proposal

> Dear subview, I give you this much space. What's your size is going to be?  
>   
> — Custom Layout negotiator

You can ask for subview size through

`func sizeThatFits(ProposedViewSize) -> CGSize`

and

`func dimensions(in: ProposedViewSize) -> ViewDimensions`

#### cache

It is a cache provided by your `makeCache(subviews:)` function. It also can be `Void` (no cache).

### Method `placeSubviews`

```swift
func placeSubviews(
    in bounds: CGRect,
    proposal: ProposedViewSize,
    subviews: Self.Subviews,
    cache: inout Self.Cache
)
```

It's where the magic happens. In this method (and only this) you are given bounds for your view and subviews for your disposal.

To place subviews, you need to call `place` method on `subviews` elements.

```swift
func place(
    at position: CGPoint,
    anchor: UnitPoint = .topLeading,
    proposal: ProposedViewSize
)
```

The definition is pretty self-explanatory. For every subview, you need to specify a point to place it, an anchor for this point, and **your** proposal for the selected subview.

#### bounds

It's bounds for your view to use. It is one of your `sizeThatFits` outputs.

💡

While it is named `bounds`, it is actually `frame`. So, the origin point is also specified **and you need to arrange subviews with respect to that**

#### proposal

The size proposal from which the container generated the size that the parent used to create the `bounds` parameter.

### About caching

You may not use it, but usually, some subviews-concerned calculations can be cached which is a good practice and great for performance.

When subviews are changed, `func updateCache(inout Self.Cache, subviews: Self.Subviews)` is called. Its default implementation is just to call `makeCache(subviews:)`.

## Creating auto-filled table

SwiftUI has a `Grid` to construct table-like structures, but what if you have an unknown number of subviews? Then, you need to construct `GridRow` somehow correctly.

Let's better use the new `Layout` protocol feature!

Subscribe and don't miss posts!

### Calculating sizes

Deciding what size the result view will have is relatively simple.

```swift
public func sizeThatFits(
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout ()
    ) -> CGSize {
    
        let subviewProposal = getSubviewProposal(
            subviewsCount: subviews.count,
            from: proposal
        )
        
        let rowHeights = getRowHeights(
        	subviews: subviews,
            globalProposal: proposal
        )
        
        let resultWidth = proposal.width ?? 
        		((subviewProposal.width ?? 0) * CGFloat(columnsNumber))
        return CGSize(
            width: resultWidth,
            height: rowHeights.reduce(0, +)
        )
    }
```

It uses several helper-functions

```swift
/**
 Get array of heights for every row.
 Just get max height on every row
 */
private func getRowHeights(subviews: Subviews, subviewProposal: ProposedViewSize) -> [CGFloat] {
    var subviewProposalNoHLimit = subviewProposal
    subviewProposalNoHLimit.height = .infinity
    
    var rowHeights = [CGFloat]()
    var index = 0
    while index < subviews.count {
        var rowMax: CGFloat = 0
        for _ in 0..<columnsNumber where index < subviews.count {
            let size = subviews[index].sizeThatFits(subviewProposalNoHLimit)
            rowMax = max(rowMax, size.height)
            index += 1
        }
        rowHeights.append(rowMax)
    }
    return rowHeights
}

/**
 Calculates proposal for subview — one cell in table
 */
func getSubviewProposal(subviewsCount: Int, from globalProposal: ProposedViewSize) -> ProposedViewSize {
    let rowHeight = max(ceil(Double(subviewsCount / columnsNumber)), 1)
    return ProposedViewSize(
        width: (globalProposal.width ?? 0)
                        / CGFloat(columnsNumber),
        height: (globalProposal.height ?? 0) / rowHeight
    )
}
```

## Placing subviews

Finally, we just need to carefully place views on their places. Just iterating over subviews and calculating their `x` and `y` position.

```swift
public func placeSubviews(
    in bounds: CGRect,
    proposal: ProposedViewSize,
    subviews: Subviews,
    cache: inout ()
) {
    var subviewProposal = getSubviewProposal(
        subviewsCount: subviews.count,
        from: proposal
    )
    let colRealWidth = subviewProposal.width ?? 0
    let rowHeights = getRowHeights(subviews: subviews, subviewProposal: subviewProposal)
    
    var curPos: CGFloat = bounds.minX
    var curHeight: CGFloat = bounds.minY
    
    var rowIndex = 0
    for (index, subview) in subviews.enumerated() {
        subviewProposal.height = rowHeights[rowIndex]
        let size = subview.dimensions(in: subviewProposal)
        
        subview.place(
            at: CGPoint(x: curPos, y: curHeight),
            anchor: .topLeading,
            proposal: subviewProposal
        )
        
        if index % columnsNumber == columnsNumber - 1 {
            curPos = bounds.minX
            curHeight += rowHeights[rowIndex]
            rowIndex += 1
        } else {
        	curPos += colRealWidth
        }
    }
}
```

## Example

Now, we can construct a table with the needed number of columns as easy as just a regular view.

![](https://alexdremov.me/content/images/2022/08/Screenshot-2022-08-11-at-23.35.29.png)

```swift
ColumnsLayout(columnsNumber: 2) {
    VStack {
        Text("That's one view")
        Image(systemName: "tortoise.fill")
    }
    .padding()
    .border(.red)
    Text("That's the second view ")
        .padding()
        .border(.red)
    Text("That's the third view with long lines that are warped automatically")
        .fixedSize(horizontal: false, vertical: true)
        .padding()
        .border(.red)
}
.border(.blue)
.padding()
```

And it magically re-assembles after changing the number of columns to three.

![](https://alexdremov.me/content/images/2022/08/Screenshot-2022-08-11-at-23.36.48.png)

## Final notes

I believe that you see how powerful this tool is. For example, [Apple creates a radial view in their example](https://developer.apple.com/documentation/swiftui/composing_custom_layouts_with_swiftui) with `Layout` protocol.

![](https://alexdremov.me/content/images/2022/08/Screenshot-2022-08-11-at-23.43.38.png)

So, it's only up to you how to place views inside your container and it's finally a room of flexibility so needed for SwiftUI in iOS 16.

[

ColumnsLayout

Complete example

ColumnsLayout.swift

4 KB

download-circle

](https://alexdremov.me/content/files/2022/08/ColumnsLayout.swift "Download")

Let me know what you think about it in the comments!