NEW: My new book Pro SwiftUI is out now – level up your SwiftUI skills today! >>

How to create a custom layout using the Layout protocol

Paul Hudson    @twostraws   

Updated for Xcode 14.2

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 here: https://www.hackingwithswift.com/store/pro-swiftui.

Hacking with Swift is sponsored by Play

SPONSORED Play is the first native iOS design tool created for designers and engineers. You can install Play for iOS and iPad today and sign up to check out the Beta of our macOS app with SwiftUI code export. We're also hiring engineers!

Click to learn more about Play!

Sponsor Hacking with Swift and reach the world's largest Swift community!

Similar solutions…

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.