skip to Main Content

So basically I need to come up with a layout that aligns the middle of a View to the bottom of other View in SwiftUI.
To make it more clear, all I need is something like this:

enter image description here

I guess the equivalent in a UIKit world would be:

redView.bottomAnchor.constraint(equalTo: whiteView.centerYAnchor)

Ive tried setting both in a ZStack and offsetting the white View but it won’t work on all screen sizes for obvious reasons.

Any tips?

3

Answers


  1. You can use .aligmentGuide (which is a tricky beast, I recommend this explanation)

    Here is your solution, its independent of the child view sizes:

    struct ContentView: View {
        
        var body: some View {
            
            ZStack(alignment: .bottom) { // subviews generally aligned at bottom
                redView
                whiteView
                    // center of this subview aligned to .bottom
                    .alignmentGuide(VerticalAlignment.bottom, 
                                    computeValue: { d in d[VerticalAlignment.center] }) 
            }
            .padding()
        }
        
        var redView: some View {
            VStack {
                Text("Red View")
            }
            .frame(maxWidth: .infinity)
            .frame(height: 200)
            .background(Color.red)
            .cornerRadius(20)
        }
        
        var whiteView: some View {
            VStack {
                Text("White View")
            }
            .frame(maxWidth: .infinity)
            .frame(width: 250, height: 100)
            .background(Color.white)
            .cornerRadius(20)
            .overlay(RoundedRectangle(cornerRadius: 20).stroke())
        }
        
    }
    
    Login or Signup to reply.
  2. You may try this:

            ZStack {
            Rectangle()
                .frame(width: 300, height: 400)
                .overlay(
                    GeometryReader { proxy in
                        
                        let offsetY = proxy.frame(in: .named("back")).midY
                        Rectangle()
                            .fill(Color.red)
                            .offset(y: offsetY)
                    }
                        .frame(width: 150, height: 140)
                    , alignment: .center)
        }
        .coordinateSpace(name: "back")
    

    Bascially the idea is to use coordinateSpace to get the frame of the bottom Rectangle and use geometryreader to get the offset needed by comparing the frame of top rectangle with the bottom one. Since we are using overlay and it is already aligned to the center horizontally, we just need to offset y to get the effect you want.

    Login or Signup to reply.
  3. Based on OPs request in comment, this is a solution making use of a custom Layout.
    The HalfOverlayLayout takes two subview and places the second half height over the first. The size of the first subview is flexible. As this is my first Layout I’m not sure if I covered all possible size variants, but it should be a start.

    struct ContentView: View {
        
        var body: some View {
            
            HalfOverlayLayout {
                redView
                whiteView
            }
            .padding()
        }
        
        
        var redView: some View {
            VStack {
                ForEach(0..<20) { _ in
                    Text("Red View")
                }
            }
            .padding()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.red)
            .cornerRadius(20)
        }
        
        var whiteView: some View {
            VStack {
                Text("White View")
            }
            .frame(width: 200, height: 150)
            .background(Color.white)
            .cornerRadius(20)
            .overlay(RoundedRectangle(cornerRadius: 20).stroke())
        }
        
    }
    
    
    
    struct HalfOverlayLayout: Layout {
        
        func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
    
            let heightContent = subviews.first?.sizeThatFits(.unspecified).height ?? 0
            let heightFooter = subviews.last?.sizeThatFits(.unspecified).height ?? 0
            let totalHeight = heightContent + heightFooter / 2
            
            let maxsizes = subviews.map { $0.sizeThatFits(.infinity) }
            var totalWidth = maxsizes.max {$0.width < $1.width}?.width ?? 0
            if let proposedWidth = proposal.width {
                if totalWidth > proposedWidth { totalWidth = proposedWidth }
            }
            return CGSize(width: totalWidth, height: totalHeight)
        }
    
        
        func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
    
            let heightFooter = subviews.last?.sizeThatFits(.unspecified).height ?? 0
            let maxHeightContent = bounds.height - heightFooter / 2
    
            var pt = CGPoint(x: bounds.midX, y: bounds.minY)
            if let first = subviews.first {
                var totalWidth = first.sizeThatFits(.infinity).width
                if let proposedWidth = proposal.width {
                    if totalWidth > proposedWidth { totalWidth = proposedWidth }
                }
                first.place(at: pt, anchor: .top, proposal: .init(width: totalWidth, height: maxHeightContent))
            }
    
            pt = CGPoint(x: bounds.midX, y: bounds.maxY)
            if let last = subviews.last {
                last.place(at: pt, anchor: .bottom, proposal: .unspecified)
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search