Prevent Text with background from expanding to maxWidth - swift

I have a Text with a background (which is just a coloured bubble), and I'm trying to restrict its maxWidth to some width, say 200 for this example.
I'm running into a problem with .frame(..., maxWidth: ...).
Expected Behvaiour
smaller messages are as big as they need to be
larger messages will have a width up to the maxWidth and will wrap without text being truncated
Actual Behaviour
no matter in which order the .background() and .frame() modifiers are applied, the Text is always drawn as what I have set the maxWidth to be.
In more detail:
Applying the .background() before the .frame(maxWidth: 200):
Text(...)
.fixedSize(horizontal: false, vertical: true)
.background(
Rectangle()
)
.frame(maxWidth: 200)
This results in the frame of all the Texts being of width 200 - which I'm not understanding. I understand that the background is applied before, so the background size is correct, but why does the frame become the maximum width? Am I misunderstanding how the maxWidth: frame works?
This behaviour is the exact same without the .fixedSize() modifier, as all that does is prevent the text from truncating into ...
Applying the .background() after the .frame(maxWidth: 200):
Text(...)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: 200)
.background(
Rectangle()
)
This shows the behaviour again, but this time with the background also affected by the frame. The modifier stacking behaviour is expected, but again, I'm not sure why the Text is expanding to the maxWidth.
For further information, these Texts are within an HStack with a Spacer() on the left side. The HStacks are within a larger VStack+ScrollView.
Removing the HStack, Spacer() or the ScrollView/VStack does not change this behaviour.
I have tried giving it a minWidth and applying other modifiers to no avail. Any help is appreciated, as I feel that I'm missing something very obvious!
Thanks.
Edit: greater concision.

You should apply .frame(maxWidth:) to the VStack, not to each Text.
struct TestView: View {
var body: some View {
VStack {
HStack {
MyText(text: "111")
MyText(text: "111")
}
MyText(text: "222222222")
MyText(text: "3333333333333")
MyText(text: "Long long long long long long long long long long text")
}
.frame(maxWidth: 200) // << there
}
struct MyText: View {
var text: String
var body: some View {
Text(text)
.padding(6)
.background(
Rectangle()
.fill(.yellow)
)
.border(.blue)
}
}
}
The blue border represents the actual Text frame sizes.
If you need a trailing alignment, then you should to set it in VStack(alignment: .trailing).
If you also need a trailing alignment inside Text frame, you can apply .multilineTextAlignment(.trailing) to Text.

Related

Responsive text box with GeometryReader

I'm trying to create responsive changes to some view parameters (e.g. font size, container box size) based on how much of a given space a text element is occupying at a given moment. The text must also be pushed to the right side of the space, which I'm accomplishing by grouping it with a Spacer() inside an HStack. Because of the space-greediness (and a related behavior I don't understand) of GeometryReader, I'm having a hard time getting the sizing variables I need without breaking the intended structure and behavior of the view.
Here's a version of the view without the responsive changes. The text appears where it should within the rounded rectangle:
struct responsiveTextBox: View {
#State var text = "Hi. "
var body: some View {
VStack {
ZStack{
RoundedRectangle(cornerRadius: 5)
.stroke()
HStack {
Spacer()
Text(text)
}
}
.frame(width: 300, height: 100)
Button("Add text") {
text += "Hi. "
}
Button("Erase text") {
text = ""
}
}
}
}
In order to implement functionality that responds to the relative amount of space occupied by the text, I need to know the size of either the Spacer() element or the Text() element itself. Wrapping the Spacer() in a GeometryReader doesn't work, because unlike the Spacer alone, the GeometryReader won't shrink to less than 50% of the HStack's width:
[...]
HStack {
GeometryReader() {freeSpace in
Spacer()
}
Text(text)
}
[...]
I don't really understand this behavior.
The alternative - wrapping the Text() element in a GeometryReader - also doesn't work, because the GeometryReader's space-greediness overrides the Spacer's, and the text appears on the left side of the rounded rectangle.
HStack {
Spacer()
GeometryReader() {textSpace in
Text(text)
}
}
What's the right way to do this?
In cases like these you can use GeometryReader in an overlay or background of the element you are interested in – so it does not affect the layout.
IMHO you also don't need the Spacer or HStack, just set the alignment on ZStack.
And ultimately you might want to look into the new Layout protocol. It seems to be the way to go for what you want to achieve. Example here
struct ContentView: View {
#State var text = "Hi. "
#State var textSize = CGSize.zero
var body: some View {
VStack {
ZStack(alignment: .trailing) {
RoundedRectangle(cornerRadius: 5)
.stroke()
Text(text)
.id(text)
.background( GeometryReader { geo in Color.clear.onAppear { textSize = geo.size }})
}
.frame(width: 300, height: 100)
Button("Add text") {
text += "Hi. "
}
Button("Erase text") {
text = ""
}
Text("Width: \(textSize.width), Height: \(textSize.height)")
}
}
}

How to keep an item exactly in the center of an HStack with other items in the HStack

I have two items in an HStack. I want one of them to be perfectly horizontally centered and the other to be on the left side (have an alignment of .leading). Here is my code so far:
HStack {
Text("1")
.frame(alignment: .leading)
Text("Hello")
.frame(alignment: .center)
}
I have figured out that the .overlay method works. However, I still want Text("Hello") to be aware of the position of Text("1") so that if Text("Hello") were a longer string, it would know not to cover up Text("1"), which is exactly what happens when the .overlay function is used. So I'm wondering if there are any other solutions.
You can add one more Text view inside HStack as hidden which have the same content as your left align text view along with Spacer. Something like this.
HStack {
Text("1")
Spacer()
Text("Good Morning, Hello World. This is long message")
.multilineTextAlignment(.center)
Spacer()
Text("1") // Set the same content as left aligned text view
.hidden()
}
.padding(.horizontal)
Preview
Not sure if it's the best way, but you can use a GeometryReader to get the screen width and skip the first half.
To place the text exactly in the center, you should also calculate the width of the text. you can use a simple extension to get the size of your text:
extension String {
public func size(withFont font: UIFont) -> CGSize {
(self as NSString).size(withAttributes: [.font: font])
}
}
Here is the example code:
GeometryReader { geo in
HStack {
Spacer()
.frame(width: (geo.size.width - "center".size(withFont: UIFont.systemFont(ofSize: 18)).width) / 2)
HStack {
Text("center")
Text("another")
}
}
}

How to center Text within a List?

I want to center Text in a List, I want to see the text in Text in the middle. I tried some solutions, but the text remains leading.
I was expecting this to work:
struct ContentView: View {
var body: some View {
List {
Text("Hello, world!")
.frame(alignment: .center)
}
}
}
The text remains leading. I also tried this, but not working:
.multilineTextAlignment(.center)
The only solution is this:
HStack(alignment: .center) {
Spacer()
Text("Hello, world!")
Spacer()
}
But I can not imagine this is the right way, I mean I add literally 3 views just to center a simple text. Is there a simpler solution? When I have a large text and I use .multilineTextAlignment(.center), the text is centered. I want it to work with 1 line of text.
You need to give all width for frame, otherwise it just fits text, so alignment has no effect.
This should work:
List {
Text("Hello, world!")
.frame(maxWidth: .infinity, alignment: .center)
}

SwiftUI Center Content alignment without supporting frames

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 2
current solution short 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:
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)
}
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]
})
}
}
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
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)
}
}

Setting the width of a textfield based on a container proxy in SwiftUI

So I have a geometry proxy that wraps a custom UITextField that I made. The only problem with the text field, is if I type in a long string, the width of the textfield gets larger to accommodate the content size. I figured the best way (I could be wrong) to deal with this is to wrap it in a geometryreader and then set the max width of the textfield as the proxy's width: Only problem is I don't know how to access the proxy's width:
HStack {
GeometryReader { proxy in
TextFieldAlternate(text: self.$bindingVariable)
.padding(.top, 10)
.padding(.leading, 15)
.padding(.bottom, 10)
.cornerRadius(10)
.frame(maxWidth: proxy.bounds.width)
}
}.padding(.horizontal)
.padding(.top, 65)
Ideally, this would like exactly like it does when the "TextFieldAlternate" is just a normal TextField. Am I thinking about this in the correct way? Because my logic was the GeometryReader will have the same dimensions as that HStack and so if I set the maxWidth of the textfield to be the same as the HStack, it won't ever get any bigger, right?