Proportional height (or width) in SwiftUI

UPDATE

If your deployment target at least iOS 16, macOS 13, tvOS 16, or watchOS 9, you can write a custom Layout. For example:

import SwiftUI

struct MyLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        return proposal.replacingUnspecifiedDimensions()
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        precondition(subviews.count == 3)

        var p = bounds.origin
        let h0 = bounds.size.height * 0.43
        subviews[0].place(
            at: p,
            proposal: .init(width: bounds.size.width, height: h0)
        )
        p.y += h0

        let h1 = bounds.size.height * 0.37
        subviews[1].place(
            at: p,
            proposal: .init(width: bounds.size.width, height: h1)
        )
        p.y += h1

        subviews[2].place(
            at: p,
            proposal: .init(
                width: bounds.size.width,
                height: bounds.size.height - h0 - h1
            )
        )
    }
}

import PlaygroundSupport
PlaygroundPage.current.setLiveView(MyLayout {
    Color.pink
    Color.indigo
    Color.mint
}.frame(width: 50, height: 100).padding())

Result:

a pink block 43 points tall atop an indigo block 37 points tall atop a mint block 20 points tall

Although this is more code than the GeometryReader solution (below), it can be easier to debug and to extend to a more complex layout.

ORIGINAL

You can make use of GeometryReader. Wrap the reader around all other views and use its closure value metrics to calculate the heights:

let propHeight = metrics.size.height * 0.43

Use it as follows:

import SwiftUI

struct ContentView: View {
    var body: some View {
        GeometryReader { metrics in
            VStack(spacing: 0) {
                Color.red.frame(height: metrics.size.height * 0.43)
                Color.green.frame(height: metrics.size.height * 0.37)
                Color.yellow
            }
        }
    }
}

import PlaygroundSupport

PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())

Leave a Comment