SwiftUI mysterious spacing between large Text and TextField in VStack - swift

I'm having trouble figuring out why there's some spacing below my text.
struct testView: View {
#State private var notes = ""
var body: some View {
VStack {
Text("Larg Text").font(.system(size: 70))
.background(Color.red)
TextField("Add a note", text: $notes)
.background(Color.red)
Spacer()
}
.background(Color.yellow)
}
}
For some reason, there's this mysterious space between Text and TextField. This space seems to decrease if I
Decrease the font size
Do not specify a font size
Don't use a TextField after Text view
Don't use a Text view before TextField
In other words, this font size–dependent spacing seems to only happen between Text and TextField. I'm utterly confused. I'd like to get rid of this space.
Appreciate your help!

It is default automatic spacing. The solution is to specify explicitly
VStack(spacing: 0) { // << here !!
Text("Larg Text").font(.system(size: 70))
.background(Color.red)
TextField("Add a note", text: $notes)
.background(Color.red)
Spacer()
}

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")
}
}
}

Spacer not working with Form inside a VStack

I'm trying to get a circle on top with the form content down below, right above my TabBar. I can somewhat force this by using .frame() but I'm not a big fan of that. It seems like there should be a simpler way in order to align it to the bottom.
My understanding is that Spacer() should push the form towards the bottom and leave the circle at the top, but this doesn't seem to be the case.
var body: some View {
VStack {
Circle().foregroundColor(.yellow).overlay(VStack {
Text("Example")
}).foregroundColor(.primary)
Spacer()
Form {
TextField("test", text: $a)
TextField("test2", text: $b)
}
}
}
All scrollviews(which Form has built on) and shapes(which Circle is) are greedy in layout priority. They don't have inner limitations so if there's available space whey gonna take it
Spacer is greedy too, but it has lower priority then other greedy views
That's why in your case Form and Circle are splitting available space 50% to 50%
You need to restrict both their height to make it work.
VStack {
Circle().foregroundColor(.yellow).overlay(VStack {
Text("Example")
}).foregroundColor(.primary)
.frame(height: UIScreen.main.bounds.width)
Spacer()
Form {
TextField("test", text: $a)
TextField("test2", text: $b)
}.frame(height: 150)
}
A way to solve this, which will look good on all devices since there are no fixed sizes, is to use SwiftUI-Introspect.
You can achieve this by getting the Form's contentHeight from the underlying UITableView.
Example:
struct ContentView: View {
#State private var a = ""
#State private var b = ""
#State private var contentHeight: CGFloat?
var body: some View {
VStack {
Circle()
.foregroundColor(.yellow)
.overlay(
VStack {
Text("Example")
}
)
.foregroundColor(.primary)
.aspectRatio(1, contentMode: .fit)
Spacer()
Form {
TextField("test", text: $a)
TextField("test2", text: $b)
}
.introspectTableView { tableView in
contentHeight = tableView.contentSize.height
}
.frame(height: contentHeight)
}
}
}
Result:
Forms, like Lists, expand to take all available space in the parent view. If you switch your simulator to Light Mode, you will see a light gray area behind your TextFields. That is your form.
What then happens is the Spacer() gets compressed to nothing. The easiest way to fix this is to put a .frame(height: ???) on the Spacer() will cause the spacer to take up that amount of space and push your Form down. One caveat is that it also pushes your circle up and shrinks it as well. I don't know if this will be an issue for you as this is a simple minimal, reproducible example, but if needed, you could set a .frame() on the upper view.
VStack {
Circle().foregroundColor(.yellow).overlay(VStack {
Text("Example")
}).foregroundColor(.primary)
.frame(width: 300)
Spacer()
.frame(height: 100)
Form {
TextField("test", text: $a)
TextField("test2", text: $b)
}
}

SwiftUI Text not shrinking with .fixedSize()

I'm working on an app with a piece of large text with buttons underneath which I want to take up the same width as the text. My solution is to use a VStack with the .fixedSize() modifier:
VStack {
Text(viewModel.timeString)
.animation(nil)
.minimumScaleFactor(0.1)
.lineLimit(1)
.font(.system(size: 100, design: .monospaced))
.padding(.vertical, -20)
TimerActionsCompact(viewModel: viewModel) // the 3 buttons
}
.fixedSize()
This produces the result I want:
However, when the text becomes too big, it just goes off the screen instead of shrinking, even though I have minimumScaleFactor and lineLimit set.
I found that the problem was .fixedSize(), and removing it causing the text to shrink properly.
Am I doing something wrong, or could this be a bug with swiftUI? If so is there any workaround?
Thanks!
One possible way you can use frame modifier like this example:
struct ContentView: View {
#State private var string: String = "0.0"
var body: some View {
Text(string)
.font(.system(size: 100, design: .monospaced))
.minimumScaleFactor(0.1)
.lineLimit(1)
.frame(width: 200, height: 80) // give your custom size here as you like!
.background(Color.gray.cornerRadius(10.0))
.animation(nil, value: string)
Button("add") { string += String(describing: Int8.random(in: 0...9)) }.padding()
}
}

SwiftUI truncate content of ForEach

In the following code snippet, the red circles obviously don't fit onto the screen and therefore only the trailing part of the HStack is shown. Instead of that behavior, I want the leading text of the HStack to always be visible and truncate the trailing red circles that don't fit into the available space (replaced with ...).
How can I do this?
struct ContentView: View {
var body: some View {
HStack {
Text("This text should always be visible")
.layoutPriority(1)
Spacer()
ForEach(0..<20) { index in
Image(systemName: "circle.fill")
.font(.body)
.foregroundColor(.red)
}
}
}
}
So I want this:
instead of this:
Can get everything except the ellipses with:
struct ContentView: View {
var body: some View {
HStack {
Text("This text should always be visible")
.fixedSize()
ForEach(0..<20) { index in
Image(systemName: "circle.fill")
.font(.body)
.foregroundColor(.red)
}
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
}
In order to get the ellipses you'll probably have to calculate frame sizes or try to get your images into an attributed string which could get pretty complicated. You might also consider a scroll view or grid.