Align elements both leading and trailing in same container, without stretching it, in SwiftUI

Viewed 31

I'm trying to build a simple chat with a design like the most usual messengers.

A message consists of a user name (if not your own message), the message itself and a timestamp.

I want the width of a message to be determined by the widest child element (most likely the message, but in rare cases maybe the user name or timestamp).

I also want the user name and message to be aligned leading/left and the timestamp to be aligned trailing/right, just like group chats in WhatsApp, Signal or the likes..

I tried weird amounts of H/V/Z-Stacks, which lead to weird happenings. I also tried both adding Spacers and maxLength = .infinity, which lead to the message stretching to the far right with lots of empty space.

My current code has all elements aligned centered:

How it's looking right now

And this is what I want:

How I want it

This code is only for the comment, which itself is embedded into a ScrollView/VStack for each one.

HStack() {
    if comment.author?.id == currentUserId {
        Spacer()
            .frame(width: 50)
        Spacer()
    }
    VStack(spacing: 3) {
        if comment.author?.id != currentUserId {
            Text("Test User")
                .font(.body)
                .frame(alignment: .leading)
                .border(.red)
        }
        Text("Test message content")
            .frame(alignment: .leading)
            .border(.red)
        Text(date)
            .foregroundColor(.gray)
            .frame(alignment: .trailing)
            .border(.red)
    }
    .border(.green)
    .padding(10)
    .background(Color.gray)
    .cornerRadius(10)
    if comment.author?.id != currentUserId {
        Spacer()
        Spacer()
            .frame(width: 50)
    }
}

Also I thought the double Spacers on the left or right side looked kinda weird. I tried using .frame(minWidth: 50), which ended up way too wide and pushes even a small message like the one on the image into a second line.

I'm happy for any ideas for these problems, but I'm trying to go for a simple solution and no complex calculations with alignmentGuides or GeometryReader or similar.

1 Answers

.frame allows you to specify the alignment.

So you could just make the VStack leading and then use .frame on the last element with alignment: .trailing. Keep in mind that the View will then grow to its available space. If you want to prevent that, just add .fixedSize() (or .fixedSize(horizontal: true, vertical: false)) to the parent view.

enter image description here

GroupBox {
    VStack(alignment: .leading) {
        Text("Test User")
        
        Text("Test Message Content")
        
        Text("9.5.2022, 19:31")
            .font(.caption)
            .foregroundColor(.secondary)
            .frame(maxWidth: .infinity, alignment: .trailing)
    }
    .fixedSize()
}

or if you want to keep the VStack on the default alignment

GroupBox {
    VStack {
        Text("Test User")
            .frame(maxWidth: .infinity, alignment: .leading)
        
        Text("Test Message Content")
        
        Text("9.5.2022, 19:31")
            .font(.caption)
            .foregroundColor(.secondary)
            .frame(maxWidth: .infinity, alignment: .trailing)
    }
    .fixedSize()
}

but it also works using a HStack + Spacer

GroupBox {
    VStack(alignment: .leading) {
        Text("Test User")
        
        Text("Test Message Content")
        
        HStack {
            Spacer()
            Text("9.5.2022, 19:31")
                .font(.caption)
                .foregroundColor(.secondary)
        }
    }
    .fixedSize()
}
Related