Skip to main content

How to create a custom layout using the Layout protocol

About 3 minSwiftSwiftUIArticle(s)bloghackingwithswift.comcrashcourseswiftswiftuixcodeappstore

How to create a custom layout using the Layout protocol 관련

SwiftUI by Example

Back to Home

How to create a custom layout using the Layout protocol | SwiftUI by Example

How to create a custom layout using the Layout protocol

Updated for Xcode 15

New in iOS 16

SwiftUI lets us create wholly custom layouts for our views using the Layout protocol, and our custom layouts can be used just like HStack, VStack, or any other built-in layout types.

Adopting the Layout protocol has just two requirements:

  • A method that returns how much space your layout wants for its subviews. This will be given a size proposal, which is how much space the parent view has available for it. This might be called multiple times so SwiftUI can see how flexible your container is.
  • Another method that actually places those subviews where you want them. This will be given the same size proposal as the first method, but will also be given a specific bounds to work with – this will be

You can also optionally make these methods cache their calculations if you're doing something particularly slow, but I've yet to encounter a situation where this is needed.

Important

When you're giving a size proposal, it might contain nil values for either or both of its width or height. As a result, it's common to call replacingUnspecifiedDimensions() on the proposal so that any nil values are replaced with a nominal, non-nil value.

For example, we could implement a radial layout – a layout that places it views around a circle:

struct RadialLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
        // accept the full proposed space, replacing any nil values with a sensible default
        proposal.replacingUnspecifiedDimensions()
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
        // calculate the radius of our bounds
        let radius = min(bounds.size.width, bounds.size.height) / 2

        // figure out the angle between each subview on our circle
        let angle = Angle.degrees(360 / Double(subviews.count)).radians

        for (index, subview) in subviews.enumerated() {
            // ask this view for its ideal size
            let viewSize = subview.sizeThatFits(.unspecified)

            // calculate the X and Y position so this view lies inside our circle's edge
            let xPos = cos(angle * Double(index) - .pi / 2) * (radius - viewSize.width / 2)
            let yPos = sin(angle * Double(index) - .pi / 2) * (radius - viewSize.height / 2)

            // position this view relative to our centre, using its natural size ("unspecified")
            let point = CGPoint(x: bounds.midX + xPos, y: bounds.midY + yPos)
            subview.place(at: point, anchor: .center, proposal: .unspecified)
        }
    }
}

We can now use that just like any other layout type. For example, we could place an array of shapes around, using a stepper to control how many are shown:

struct ContentView: View {
    @State private var count = 16

    var body: some View {
        RadialLayout {
            ForEach(0..<count, id: \.self) { _ in
                Circle()
                    .frame(width: 32, height: 32)
            }
        }
        .safeAreaInset(edge: .bottom) {
            Stepper("Count: \(count)", value: $count.animation(), in: 0...36)
                .padding()
        }
    }
}

My book Pro SwiftUI goes into a lot more detail on custom layouts, including SwiftUI code for masonry layouts, equal width stacks, relative stacks, layout caches, custom animations, and more. Find out more hereopen in new window.

Similar solutions…
How to fix “Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements” | SwiftUI by Example

How to fix “Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements”
How to create and compose custom views | SwiftUI by Example

How to create and compose custom views
How to create a custom transition | SwiftUI by Example

How to create a custom transition
How to create custom animated drawings with TimelineView and Canvas | SwiftUI by Example

How to create custom animated drawings with TimelineView and Canvas
How to create custom bindings | SwiftUI by Example

How to create custom bindings

이찬희 (MarkiiimarK)
Never Stop Learning.