2

Here is a breakdown.

I have a zstack that contains 2 vstacks.

first vstack has a spacer and an image second has a text and button.

ZStack {
    VStack {
        Spacer()
        Image("some image")
    }
    VStack {
        Text("press the button")
        Button("ok") {
            print("you pressed the button")
        }
    }
}

Now this setup would easily give me an image on the bottom of a zstack, and a centered title and button.

However if for example the device had a small screen or an ipad rotates to landscape. depending on the image size (which is dynamic). The title and button will overlap the image. instead of the button being "pushed" up.

In UIKit this is as simple as centering the button to superview with a high priority and having greaterThanOrEqualTo image.topAnchor with a required priority.

button would be centered in screen but if the top of the image was too big the center constraint would give priority to the image top anchor required constraint and push the button up.

I have looked into custom alignments and can easily get always above image or always center but am missing some insight in having it both depending on layout. Image size is dynamic so no hardcoded sizes.

What am i missing here? how would you solve this simple yet tricky task.

Alessio
  • 21
  • 4

1 Answers1

1

There might be an easier way using .alignmentGuide but I tried to practice on Layout for this answer.

I created a custom ImageAndButtonLayout that should do what you want: it takes two views assuming the first is the image and the second is the button (or anything else).

They are put into subviews just for clarity, you can also put them directly into ImageAndButtonLayout. For testing you can change the height of the image via slider.

The Layout always uses the available full height and pushes the first view (image) to the bottom - so you don't need an extra Spacer() with the image. The position of the second view (button) is calculated based on the height of the first view and the available height.

enter image description here

struct ContentView: View {
    
    @State private var imageHeight = 200.0 // for testing
    
    var body: some View {
        
        VStack {
            
            ImageAndButtonLayout {
                imageView
                buttonView
            }
            
            // changing "image" height for testing
            Slider(value: $imageHeight, in: 50...1000)
                .padding()
        }
    }
    
    
    var imageView: some View {
        Color.teal // Image placeholder
            .frame(height: imageHeight)
    }
    
    var buttonView: some View {
        VStack {
            Text("press the button")
            Button("ok") {
                print("you pressed the button")
            }
        }
    }
    
}



struct ImageAndButtonLayout: Layout {
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        
        let maxsizes = subviews.map { $0.sizeThatFits(.infinity) }
        
        var totalWidth = maxsizes.max {$0.width < $1.width}?.width ?? 0
        totalWidth = min(totalWidth, proposal.width ?? .infinity )
        
        let totalHeight = proposal.height ?? .infinity // always return maximum height
        
        return CGSize(width: totalWidth, height: totalHeight)
    }
    
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        
        let heightImage = subviews.first?.sizeThatFits(.unspecified).height ?? 0
        let heightButton = subviews.last?.sizeThatFits(.unspecified).height ?? 0
        let maxHeightContent = bounds.height
        
        // place image at bottom, growing upwards
        let ptBottom = CGPoint(x: bounds.midX, y: bounds.maxY) // bottom of screen
        if let first = subviews.first {
            var totalWidth = first.sizeThatFits(.infinity).width
            totalWidth = min(totalWidth, proposal.width ?? .infinity )
            first.place(at: ptBottom, anchor: .bottom, proposal: .init(width: totalWidth, height: maxHeightContent))
        }
        
        // place button at center – or above image
        var centerY = bounds.midY
        if heightImage > maxHeightContent / 2 - heightButton {
            centerY = maxHeightContent - heightImage
            centerY = max ( heightButton * 2 , centerY ) // stop at top of screen
        }
        let ptCenter = CGPoint(x: bounds.midX, y: centerY)
        if let last = subviews.last {
            last.place(at: ptCenter, anchor: .center, proposal: .unspecified)
        }
    }
}
ChrisR
  • 9,523
  • 1
  • 8
  • 26
  • Interesting take, havent done much with layout. Will have to give it a try. Am curious though on how it could be done with alignmentGuide if at all – Alessio Feb 18 '23 at 21:44