Search code examples
iosswiftswiftuiswiftui-zstack

Why does adding a HStack inside a VStack wreck size calculations for a ZStack bubble around text elements?


I want to produce a bubble-like view with various pieces of text on a rounded rectangle background, sized to fit the text. I have arrived at this:

    var body: some View {
        HStack {
            Spacer()
            ZStack(alignment: .trailing) {
                Color.blue
                VStack{
                    Text(message.content)
                        .padding(.all, .oneUnit)
                        .multilineTextAlignment(.trailing)
                }
            }
            .clipShape(RoundedRectangle(cornerRadius: 12))
        }
    }

When that is repeated with a ForEach and some text passed in as message, content is almost right but the ZStack background should only be big enough to cover the text, making a bubble:

ZStack is too large - full width though text is small

Using .layoutPriority fixes this, code becomes:

    var body: some View {
        HStack {
            Spacer()
            ZStack(alignment: .trailing) {
                Color.blue
                VStack{
                    Text(message.content)
                        .padding(.all, .oneUnit)
                        .multilineTextAlignment(.trailing)
                }
                .layoutPriority(1)
            }
            .clipShape(RoundedRectangle(cornerRadius: 12))
        }
    }

Correct size - fits text

So the size of the ZStack is the size required to fit the text. Great! Now to put all the required pieces of text in the VStack. Let's add one more Text:

    var body: some View {
        HStack {
            Spacer()
            ZStack(alignment: .trailing) {
                Color.blue
                VStack{
                    Text(message.content)
                        .padding(.all, .oneUnit)
                        .multilineTextAlignment(.trailing)
                    Text("Out damned spot!")
                }
                .layoutPriority(1)
            }
            .clipShape(RoundedRectangle(cornerRadius: 12))
        }
    }

Text display underneath, sizing still correct.

VStack with two text elements is correctly sized

But that last piece of text has to be right-aligned. So then the code is:

    var body: some View {
        HStack {
            Spacer()
            ZStack(alignment: .trailing) {
                Color.blue
                VStack{
                    Text(message.content)
                        .padding(.all, .oneUnit)
                        .multilineTextAlignment(.trailing)
                    HStack {
                        Spacer()
                        Text("Out damned spot!")
                    }
                }
                .layoutPriority(1)
            }
            .clipShape(RoundedRectangle(cornerRadius: 12))
        }
    }

And then everything is broken. Can anyone say why the HStack breaks the ZStack's size?

ZStack becomes full-width - incorrect!


Solution

  • @SwissMark is right, the cause of your problem is that Spacer() is greedy, it takes all the space left. Making your bubbles the max size possible. But only making VStack alignment .trailing doesn't do the trick.

    Problems:

    1. Your background is Set in the ZStack
    2. layoutPriority doesn't make any sense here

    Here is the solution:

    import SwiftUI
    
    struct ContentView: View {
        
        var messages = [
            "I heard the owl scream and the crickets cry.",
            "Out damned spot!",
            "Did not you speak?",
            "Out damned spot!",
            "When?",
            "Out damned spot!",
            "Now.",
            "Out damned spot!"
        ]
        
        var body: some View {
            HStack {
                Spacer()
                ZStack(alignment: .trailing) {
                    VStack(alignment: .trailing) {
                        VStack(alignment: .trailing) {
                            Text(messages[0])
                                .padding(.all, 4)
                                .multilineTextAlignment(.trailing)
                            Text(messages[1])
                                .padding([.bottom, .trailing], 4)
                        }
                        .padding(4)
                        .background(Color.blue)
                        .clipShape(RoundedRectangle(cornerRadius: 12))
                        
                        VStack(alignment: .trailing) {
                            Text(messages[2])
                                .padding(.all, 4)
                                .multilineTextAlignment(.trailing)
                            Text(messages[3])
                                .padding([.bottom, .trailing], 4)
                        }
                        .padding(4)
                        .background(Color.blue)
                        .clipShape(RoundedRectangle(cornerRadius: 12))
                        
                        VStack(alignment: .trailing) {
                            Text(messages[4])
                                .padding(.all, 4)
                                .multilineTextAlignment(.trailing)
                            Text(messages[5])
                                .padding([.bottom, .trailing], 4)
                        }
                        .padding(4)
                        .background(Color.blue)
                        .clipShape(RoundedRectangle(cornerRadius: 12))
                        
                        VStack(alignment: .trailing) {
                            Text(messages[6])
                                .padding(.all, 4)
                                .multilineTextAlignment(.trailing)
                            Text(messages[7])
                                .padding([.bottom, .trailing], 4)
                        }
                        .padding(4)
                        .background(Color.blue)
                        .clipShape(RoundedRectangle(cornerRadius: 12))
                    }
                }
            }
        }
    }
    

    Final image:

    enter image description here

    Modify the paddings and spaces as you wish.