< How to dynamically change between VStack and HStack | How to create an adaptive layout with ViewThatFits > |
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:
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.
SPONSORED Build a functional Twitter clone using APIs and SwiftUI with Stream's 7-part tutorial series. In just four days, learn how to create your own Twitter using Stream Chat, Algolia, 100ms, Mux, and RevenueCat.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.