Arrange views in your app’s interface using layout tools that SwiftUI provides.

Last update: Jun 9, 2022

Composing custom layouts with SwiftUI

Arrange views in your app's interface using layout tools that SwiftUI provides.

Overview

This sample app demonstrates many of the layout tools that SwiftUI provides by building an interface that enables people to vote for their favorite kind of pet. The app offers buttons to vote for a specific pet type, and displays the vote counts and relative rankings of the various contenders on a leaderboard. It also shows avatars for the pets, arranged in a way that reflects the current rankings.

Arrange views in two dimensions with a grid

To draw a leaderboard in the middle of the display that shows vote counts and percentages, the sample uses a Grid view.

Grid(alignment: .leading) {
    ForEach(model.pets) { pet in
        GridRow {
            Text(pet.type)
            ProgressView(
                value: Double(pet.votes),
                total: Double(max(1, model.totalVotes))) // Avoid dividing by zero.
            Text("\(pet.votes)")
                .gridColumnAlignment(.trailing)
        }

        Divider()
    }
}

View in Source

The grid contains a GridRow inside a ForEach, where each view in the row creates a column cell. So the first view appears in the first column, the second in the second column, and so on. Because the Divider appears outside of a grid row instance, it creates a row that spans the width of the grid.

The sample initializes the grid with leading-edge alignment, which applies to every cell in the grid. Meanwhile, the gridColumnAlignment(_:) view modifier that appears on the vote count cell overrides the alignment of cells in that column to use trailing-edge alignment.

Create a custom equal-width layout

The app offers buttons for voting at the bottom of the interface. To ensure the buttons all have the same width, but are no wider than the widest button text, the app creates a custom layout container type that conforms to the Layout protocol. The equal-width horizontal stack (MyEqualWidthHStack) measures the ideal sizes of all its subviews, and offers the widest ideal size to each subview.

The custom stack implements the protocol's two required methods. First, sizeThatFits(proposal:subviews:cache:) reports the container's size, given a set of subviews.

func sizeThatFits(
    proposal: ProposedViewSize,
    subviews: Subviews,
    cache: inout Void
) -> CGSize {
    guard !subviews.isEmpty else { return .zero }

    let maxSize = maxSize(subviews: subviews)
    let spacing = spacing(subviews: subviews)
    let totalSpacing = spacing.reduce(0) { $0 + $1 }

    return CGSize(
        width: maxSize.width * CGFloat(subviews.count) + totalSpacing,
        height: maxSize.height)
}

View in Source

This method combines the largest size in each dimension with the horizontal spacing between subviews to find the container's total size. Then, placeSubviews(in:proposal:subviews:cache:) tells each of the subviews where to appear within the layout's bounds.

func placeSubviews(
    in bounds: CGRect,
    proposal: ProposedViewSize,
    subviews: Subviews,
    cache: inout Void
) {
    guard !subviews.isEmpty else { return }

    let maxSize = maxSize(subviews: subviews)
    let spacing = spacing(subviews: subviews)

    let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height)
    var nextX = bounds.minX + maxSize.width / 2

    for index in subviews.indices {
        subviews[index].place(
            at: CGPoint(x: nextX, y: bounds.midY),
            anchor: .center,
            proposal: placementProposal)
        nextX += maxSize.width + spacing[index]
    }
}

View in Source

The method creates a single size proposal for the subviews, and then uses that, along with a point that changes for each subview, to arrange the buttons in a horizontal line with default spacing.

Choose the view that fits

The size of the voting buttons depends on the width of the text they contain. For people that speak another language, or that use a larger text size, the horizontally arranged buttons might not fit in the display. So the app uses ViewThatFits to let SwiftUI choose between a horizontal and a vertical arrangement of the buttons for the one that fits in the available space.

ViewThatFits { // Choose the first view that fits.
    MyEqualWidthHStack { // Arrange horizontally if it fits...
        Buttons()
    }
    MyEqualWidthVStack { // ...or vertically, otherwise.
        Buttons()
    }
}

View in Source

To ensure that the buttons maintain their equal-width property when arranged vertically, the app uses a custom equal-width vertical stack (MyEqualWidthVStack) that's very similar to the horizontal version.

Improve layout efficiency with a cache

The methods of the Layout protocol take a bidirectional cache parameter. The cache provides access to optional storage that's shared among all the methods of a particular layout instance. To demonstrate the use of a cache, the sample app's equal-width vertical layout creates storage to share size and spacing calculations between its sizeThatFits(proposal:subviews:cache:) and placeSubviews(in:proposal:subviews:cache:) implementations.

First, the layout defines a CacheData type for the storage.

struct CacheData {
    let maxSize: CGSize
    let spacing: [CGFloat]
    let totalSpacing: CGFloat
}

View in Source

It then implements the protocol's optional makeCache(subviews:) method to do the calculations for a set of subviews, returning a value of the type defined above.

func makeCache(subviews: Subviews) -> CacheData {
    let maxSize = maxSize(subviews: subviews)
    let spacing = spacing(subviews: subviews)
    let totalSpacing = spacing.reduce(0) { $0 + $1 }

    return CacheData(
        maxSize: maxSize,
        spacing: spacing,
        totalSpacing: totalSpacing)
}

View in Source

If the subviews change, SwiftUI calls the layout's updateCache(_:subviews:) method. The default implementation of that method calls makeCache(subviews:) again, which recalculates the data. Then the sizeThatFits(proposal:subviews:cache:) and placeSubviews(in:proposal:subviews:cache:) methods make use of their cache parameter to retrieve the data. For example, placeSubviews(in:proposal:subviews:cache:) reads the size and the spacing array from the cache.

let maxSize = cache.maxSize
let spacing = cache.spacing

View in Source

Contrast this with the equal-width horizontal stack, which doesn't use a cache, and instead calculates the size and spacing information every time it needs that information.

  • Note: Most simple layouts, including the equal-width vertical stack, don't gain much efficiency from using a cache. Developers can profile their app with Instruments to find out whether a particular layout type actually benefits from a cache.

Create a custom radial layout with an offset

To display the pet avatars in a circle, the app defines a radial layout (MyRadialLayout). Like other custom layouts, this layout needs the two required methods. For sizeThatFits(proposal:subviews:cache:), the layout fills the available space by returning whatever size its container proposes.

return proposal.replacingUnspecifiedDimensions()

View in Source

The app uses the proposal's replacingUnspecifiedDimensions(by:) method to convert the proposal into a concrete size. Then, to place subviews, the layout rotates a vector, translates the vector to the middle of the placement region, and uses that as the anchor for the subview.

for (index, subview) in subviews.enumerated() {
    // Find a vector with an appropriate size and rotation.
    var point = CGPoint(x: 0, y: -radius)
        .applying(CGAffineTransform(
            rotationAngle: angle * Double(index) + offset))

    // Shift the vector to the middle of the region.
    point.x += bounds.midX
    point.y += bounds.midY

    // Place the subview.
    subview.place(at: point, anchor: .center, proposal: .unspecified)
}

View in Source

The offset that the app applies to the rotation accounts for the current rankings, placing higher-ranked pets closer to the top of the interface. The app stores ranks on the subviews using the LayoutValueKey protocol, and then reads the values to calculate the offset before placing views.

Animate transitions between layouts

The radial layout can calculate an offset that creates an appropriate arrangement for all but one set of rankings: there's no way to show a three-way tie with the avatars in a circle. To resolve this, the app detects this condition, and uses it to put the avatars in a line instead, using a built-in HStack. To transition between these layout types, the app uses the AnyLayout type.

let layout = model.isAllWayTie ? AnyLayout(HStack()) : AnyLayout(MyRadialLayout())

Podium()
    .overlay(alignment: .top) {
        layout {
            ForEach(model.pets) { pet in
                Avatar(pet: pet)
                    .rank(model.rank(pet))
            }
        }
        .animation(.default, value: model.pets)
    }

View in Source

Because the structural identity of the views remains the same throughout, the animation(_:value:) view modifier creates animated transitions between layout types. The modifier also animates radial layout changes that result from changes in the rankings because the calculated offsets depend on the same pet data.

GitHub

https://github.com/apple-sample-code/ComposingCustomLayoutsWithSwiftUI
You might also like...

Auto Layout (and manual layout) in one line.

Auto Layout (and manual layout) in one line.

Auto Layout (and manual layout) in one line. Quick Look view.bb.centerX().below(view2).size(100) It’s equivalent to iOS 9 API: view.centerXAnchor.cons

May 17, 2022

Auto Layout made easy with the Custom Layout.

Auto Layout made easy with the Custom Layout.

Auto Layout made easy with the Custom Layout. Getting started CocoaPods CocoaPods is a dependency manager for Cocoa projects. You can install it with

Jan 16, 2022

An experiment creating a particle emitter using the new TimelineView and Canvas views in SwiftUI

An experiment creating a particle emitter using the new TimelineView and Canvas views in SwiftUI

Particle Emitter An experiment creating a particle emitter using the new Timelin

Apr 17, 2022

This is a simple chat application made in Swift using send and receive interface.

Flash Chat 💁🏽‍♂️ Overview This is a simple chat application made in Swift using send and receive interface. ⚙️ How it works The user needs to first

Mar 20, 2022

StoryboardUsingCustomViews - Storyboard Using Custom Views

StoryboardUsingCustomViews - Storyboard Using Custom Views

Storyboard Using Custom Views Vista creada con: Storyboard + Constraints + Progr

Jan 19, 2022

Flexbox in Swift, using Facebook's css-layout.

SwiftBox A Swift wrapper around Facebook's implementation of CSS's flexbox. Example let parent = Node(size: CGSize(width: 300, height: 300),

Jun 23, 2022

SuperLayout is a Swift library that makes using Auto Layout a breeze.

SuperLayout is a Swift library that makes using Auto Layout a breeze.

SuperLayout is a library that adds a few custom operators to Swift that makes using the amazing NSLayoutAnchor API for Auto Layout a breeze. SuperLayo

Jan 11, 2021

Content Hugging Priority settings using Auto Layout

AutoLayoutContentHugging Swift 5 and Xcode 12. Content Hugging Priority settings using Auto Layout. Content Hugging Priority give you granular control

Jan 21, 2022

SwiftUI stack views with paged scrolling behaviour.

SwiftUI PageView SwiftUI stack views with paged scrolling behaviour. HPageView A view that arranges its children in a horizontal line, and provides pa

Jun 6, 2022
SwiftUI views that arrange their children in a Pinterest-like layout
SwiftUI views that arrange their children in a Pinterest-like layout

SwiftUI Masonry SwiftUI views that arrange their children in a Pinterest-like layout. HMasonry A view that arranges its children in a horizontal mason

Jun 10, 2022
Reframing SwiftUI Views. A collection of tools to help with layout.
Reframing SwiftUI Views. A collection of tools to help with layout.

Overview A Swift Package with a collection of SwiftUI framing views and tools to help with layout. Size readers like WidthReader, HeightReader, and on

Jun 20, 2022
AppStoreClone - Understanding the complex layout of app store using UICompositional layout in swift
AppStoreClone - Understanding the complex layout of app store using UICompositional layout in swift

AppStoreClone Understanding the complex layout of app store using UICompositiona

Feb 18, 2022
Apple provides us two ways to use UIKit views in SwiftUI

RepresentableKit Apple provides us two ways to use UIKit views in SwiftUI: UIVie

May 19, 2022
Expose layout margins and readable content width to SwiftUI's Views

SwiftUI Layout Guides This micro-library exposes UIKit's layout margins and readable content guides to SwiftUI. Usage Make a view fit the readable con

Jun 22, 2022
Powerful autolayout framework, that can manage UIView(NSView), CALayer and not rendered views. Not Apple Autolayout wrapper. Provides placeholders. Linux support.
Powerful autolayout framework, that can manage UIView(NSView), CALayer and not rendered views. Not Apple Autolayout wrapper. Provides placeholders. Linux support.

CGLayout Powerful autolayout framework, that can manage UIView(NSView), CALayer and not rendered views. Has cross-hierarchy coordinate space. Implemen

Jan 29, 2022
Swift-picker-views - inline single and multi picker views for UIKit. Without tableview! Easy and simple
Swift-picker-views - inline single and multi picker views for UIKit. Without tableview! Easy and simple

swift-picker-views Inline single and multiple picker views for UIKit. No tablevi

Jan 31, 2022
A Swift utility to make updating table views/collection views trivially easy and reliable.

ArrayDiff An efficient Swift utility to compute the difference between two arrays. Get the removedIndexes and insertedIndexes and pass them directly a

Jun 5, 2022
Fast Swift Views layouting without auto layout. No magic, pure code, full control and blazing fast. Concise syntax, intuitive, readable & chainable. [iOS/macOS/tvOS/CALayer]
Fast Swift Views layouting without auto layout. No magic, pure code, full control and blazing fast. Concise syntax, intuitive, readable & chainable. [iOS/macOS/tvOS/CALayer]

Extremely Fast views layouting without auto layout. No magic, pure code, full control and blazing fast. Concise syntax, intuitive, readable & chainabl

Jun 27, 2022
Fast Swift Views layouting without auto layout. No magic, pure code, full control and blazing fast
Fast Swift Views layouting without auto layout. No magic, pure code, full control and blazing fast

Fast Swift Views layouting without auto layout. No magic, pure code, full control and blazing fast. Concise syntax, intuitive, readable & chainable. [iOS/macOS/tvOS/CALayer]

Jun 20, 2022