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

Overview

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.

You might also like...
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]

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

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

⌨️ KeyboardToolbar - Add tools above your keyboard with iOS-like keyboard buttons
⌨️ KeyboardToolbar - Add tools above your keyboard with iOS-like keyboard buttons

KeyboardToolbar 👀 Overview Use KeyboardToolbar to add tools as an input accessory view to a UITextField, UITextView, or any other view conforming to

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

A Set of Tools To Extend UIKit (Classic iOS Framework)
A Set of Tools To Extend UIKit (Classic iOS Framework)

RVS_UIKit_Toolbox A set of basic UIKit tools, for Swift iOS app development. Overview This package offers a few extensions of standard UIKit classes,

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

StoryboardUsingCustomViews - Storyboard Using Custom Views
StoryboardUsingCustomViews - Storyboard Using Custom Views

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

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),

Owner
Apple Sample Code
Repositories mirroring Apple sample code
Apple Sample Code
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

Ciaran O'Brien 88 Dec 27, 2022
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

Ryan Lintott 84 Dec 16, 2022
AppStoreClone - Understanding the complex layout of app store using UICompositional layout in swift

AppStoreClone Understanding the complex layout of app store using UICompositiona

Dheeraj Kumar Sharma 8 Dec 28, 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

YUMEMI Inc. 43 Dec 26, 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

Thomas Grapperon 26 Dec 23, 2022
A custom layout built on top of SwiftUI's Layout API that lays elements out in multiple lines. Similar to flex-wrap in CSS, CollectionViewFlowLayout.

WrapLayout A custom layout built on top of SwiftUI's Layout API that lays elements out in multiple lines. Similar to flex-wrap in CSS, CollectionViewF

Hiroshi Kimura 6 Sep 27, 2022
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

IBRAHIM YILMAZ 2 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

Adlai Holler 100 Jun 5, 2022
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

Koryttsev Denis 45 Jun 28, 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]

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

layoutBox 2.1k Dec 22, 2022