Search code examples
swiftuser-interfaceswiftuilayout

How do I offset a view in an HStack but still constrain the frame to match with the offset?


As the title suggests, I'm looking to offset some views, a Circle inside of an HStack, however in doing so, this results in weird behavior where the frame of the HStack is no longer properly wrapping the contents. I need this "Offset" action to still allow proper leading and trailing padding, without empty space there. I considered using a ZStack as well, however I feel like that might be cumbersome as I need it to "Wrap" my contents, rather than take all space available.

enter image description here

HStack {
    ForEach(novaraUsers.indices, id: \.self) { index in
        let user = novaraUsers[index]
        
        if index < visibleUserCount + 1 {
            Group {
                if index < visibleUserCount {
                    Circle()
                        .fill(.white)
                        .stroke(Color.getRandomColor(from: user.username), lineWidth: 4)
                        .foregroundColor(.white)
                        .frame(width: 50, height: 50)
                        .overlay(
                            Text(user.username.prefix(1))
                                .font(.title)
                                .foregroundColor(.black)
                                .font(.h1)
                        )
                } else {
                    Circle()
                        .fill(.white)
                        .stroke(Color.getRandomColor(from: String(novaraUsers.count)), lineWidth: 4)
                        .foregroundColor(.white)
                        .frame(width: 50, height: 50)
                        .overlay(
                            Text("\(moreThanCount)+")
                                .font(.title)
                                .foregroundColor(.black)
                                .font(.h1)
                        )
                }
            }
            .offset(x: CGFloat(offsetPerCircle * index) * -1)
        }
    }
}
.frame(width: CGFloat(50 * (visibleUserCount + moreThanCount)))
.border(.black, width: 2)

Solution

  • One way to achieve this layout is to switch the way the circles are drawn. Instead of having a circle as the base view with the text as an overlay, use the text as the base view and apply the circle as background. If the circle is slightly larger than the base view (the text) then you get the overlap effect you are after and no offset needs to be used at all.

    Like this:

    Group {
        if index < visibleUserCount {
            Text(user.username.prefix(1))
                .font(.title)
                .foregroundColor(.black)
                .font(.h1)
                .frame(width: 35, height: 35)
                .background {
                    Circle()
                        .inset(by: -7.5)
                        .fill(.white)
                        .stroke(Color.getRandomColor(from: user.username), lineWidth: 4)
                        .foregroundColor(.white)
                }
        } else {
            Text("\(moreThanCount)+")
                .font(.title)
                .foregroundColor(.black)
                .font(.h1)
                .frame(width: 35, height: 35)
                .background {
                    Circle()
                        .inset(by: -7.5)
                        .fill(.white)
                        .stroke(Color.getRandomColor(from: String(novaraUsers.count)), lineWidth: 4)
                        .foregroundColor(.white)
                }
        }
    }
    // Not needed
    //.offset(x: CGFloat(offsetPerCircle * index) * -1)
    
    

    Players