2

I'm having trouble trying to center a single element to emulate the navigation modal with a close button. I would like to center content without using a supporting Rectangle on the sides or spacers.

What i'm trying to achieve is whenever the text grow, if it reaches the left sides where there is the close xmark button it should try to push itself on the right where there is available space until it reaches the right border and after wrap itself if there are no available space on the both sides.

here are some pictures:

expected result 1

expected result 1

expected result 2

expected result 2

current solution short text

current solution short text

current solution long text

current solution long text

i tried using long and short text to test the content behaviour

Currently this is the start of the code and basically i would like to avoid to add the blue rectangle (that would be usually clear)

   struct TestAlignmentSwiftUIView: View {
        var body: some View {
            HStack(spacing: 0) {
                Rectangle().fill(Color.blue).frame(width: 44, height: 44)
    
                Text("aaa eee aaa")
                    .background(Color.red)
                    .padding(5)
    
                Button(action: {}, label: {
                    Image(systemName: "xmark")
                        .padding(15)
                        .frame(width: 44, height: 44)
                        .background(Color.yellow)
                })
            }
            .background(Color.green)
        }
    }

What i've tried so far but doesn't resolve the issue if the code inside the text component grow:

  1. Using a zstack where i place the text and the close button one on top of each other but the button is pushed to the side using a spacer. It will work for small text or content but is not scalable if the text grows
    var body: some View {
        ZStack {
            HStack {
                Spacer()
                Button(action: {}, label: {
                    Image(systemName: "xmark")
                        .padding(15)
                        .frame(width: 44, height: 44)
                        .background(Color.yellow)
                })
            }
            Text("aaa eee aaa random long very long text that should wrap without overlapping. long text")
                .background(Color.red)
                .frame(maxWidth: .infinity, alignment: .center)
                .padding(5)
                .opacity(0.7)
        }
        .background(Color.green)
    }
  1. Using alignment guides : i would create my own center alignment guide, then use this custom alignment on a vstack where i place my content plus a fake filler rectangle that should center the elements on the content side.

the problem is that with swiftui , as far i know, you can only align one descendant element, and doesn't support multiple custom alignments on the stack of elements. so i would have only the text centered or the side button aligned not both aligned one to the center and the other to the trailing edge. and if i put a spacer between them it will just mess the alignment created. If I try with small text they will be both attached. Heres the code. try to comment the button and you will see that it will center itself or add spacer between them.

extension HorizontalAlignment {
    private enum MyAlignment: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat {
            d[HorizontalAlignment.center]
        }
    }
    static let myAlignment = HorizontalAlignment(MyAlignment.self)
}
var body: some View {
        VStack(alignment: .myAlignment, spacing: 0) {
            HStack {
                Text("aaa eee aaa random  ")
                    .background(Color.red)
                    .frame(maxWidth: .infinity, alignment: .center)
                    .padding(5)
                    .alignmentGuide(.myhAlignment, computeValue: { dimension in
                        dimension[HorizontalAlignment.center]
                    })

                Button(action: {}, label: {
                    Image(systemName: "xmark")
                        .padding(15)
                        .frame(width: 44, height: 44)
                        .background(Color.yellow)
                })
            }
            .background(Color.green)
            Rectangle()
                .fill(Color.purple)
                .frame(width: 10, height: 10, alignment: .center)

                .alignmentGuide(.myhAlignment, computeValue: { dimension in
                    dimension[HorizontalAlignment.center]
                })
        }
    }

  1. Tried with a combination of geometry reader and/or anchor preferences to read with sizes of the text content and side button width and apply the appropriate center offset manually, but it seems too hacky and it never worked as expected without good results

If you're familiar with uikit this problem would be resolved using a centerX on the container with a minor layout priority and a right constraint from the center to the close button, and call it a day. But on swiftui it seems soo hard to handle this simple cases.

So far i haven't found a solution without using a supporting fixed frame on the side that would work with both long and short text. that space is clearly visibile if you try to use long text. and it will leave the user to wonder why is not used.

¯\ (ツ)

EDIT: added possible solution in the answers

7
  • I just wish that I could ask my questions as a new contributor like you did here. Without any other users trying to correcting your question! I think it needs lots of luck. Commented Aug 5, 2021 at 22:54
  • Have you tried to play with .overlay Commented Aug 6, 2021 at 6:56
  • yep, it depends if with .overlay you mean using with a spacers on the sides and then it would be something like Spacer() Text() Spacer().overlay(Button) the problem with .overlay is that i lose information about the the button size. and if the text grow it will overlap. Don't know if you meant with '.overlay(Geometryreader { ... ) ' to get the size of the Button, but same problems of overlaps regardless. Commented Aug 6, 2021 at 8:06
  • You can add .hidden() to the blue rectangle or .opacity(0) then it would disappear while keeping the layout as is Commented Aug 6, 2021 at 9:56
  • 1
    I think you are on the right track using the filler view on the left. And I think shrinking the blue square when the text is ready to wrap is going to do what you want. @Asperi had a solution to a similar problem with regard to TextFields that may give you what you need. Essentially you use another Text() and keep track of its size and then use that to set the size of the displayed Text(). Commented Aug 6, 2021 at 14:25

1 Answer 1

0

From the @Yrb suggestion in the comments, here's what i came up that shrink the blue size so it will center on the available space. I added a fake text underneath and tracked the size. and if it's over the available space i will take the difference and shrink the blu rectangle. One thing to keep in mind is that the hidden content if contains some text should have linelimit 1, otherwise it will get a smaller size from wrapping itself.

And i just assume that i know the size of the close button (or at least one side) for center alignment, and even if i don't know it at compile time, i could probably use a preference key to get the size at run time, and have it dynamic.

But for the moment i think it's fine the result that i got. but honestly i hope to find something more easier in the future.

@State var text: String = "aaa eee aaa"
@State private var fillerWidth: CGFloat = 44
// i assume i know the max size of the close button or at least one side
private let kCloseButtonWidth: CGFloat = 44 

private struct FakeSizeTitlteContentKey: PreferenceKey {
    static var defaultValue: CGFloat { .zero }
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

var body: some View {
    ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
        GeometryReader { parentGeometry in
            titleContent
                .lineLimit(1) // hidden text must not wrap
                .overlay(GeometryReader { proxyFake in
                    Color.clear.border(Color.black, width: 0.3)
                        .preference(key: FakeSizeTitlteContentKey.self, value: proxyFake.frame(in: .local).width
                        .onPreferenceChange(FakeSizeTitlteContentKey.self) { value in
                            let availableW = parentGeometry.frame(in: .local).width
                            let fillSpace = availableW - value - kCloseButtonWidth * 2
                            fillerWidth = min(kCloseButtonWidth, max(0, fillSpace))
                        }
                })
        }
        .hidden()
        VStack {
            HStack(spacing: 0) {
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: fillerWidth, height: 44)
                titleContent
                    .background(Color.green)
                    .multilineTextAlignment(.center)
                    .frame(maxWidth: .infinity, alignment: .center)
                Button(action: {}, label: {
                    Image(systemName: "xmark")
                        .padding(15)
                        .frame(width: kCloseButtonWidth, height: kCloseButtonWidth)
                        .background(Color.yellow)
                })
            }
            .coordinateSpace(name: "fullCont")
            .background(Color.green)
            
            TextEditor(text: $text)
                .frame(maxHeight: 150, alignment: .center)
                .border(Color.black, width: 1)
                .padding(15)
            Spacer()
        }
    }
}

@ViewBuilder var titleContent: some View {
    HStack(spacing: 0) {
        Text(text)
            .background(Color.red)
            .padding(.horizontal, 5)
    }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.