SwiftUI: Text mis-alignment when inside Button (bug?) - swiftui

So this is a weird one, and it looks like a bug. When placed inside a button, title that cannot fit on one line mis-aligns. It should be aligned to the left, but shows up centered.
Everything latest, XCode 13.2.1
MRE is below, any tips much appreciated!
HStack {
HStack{
Image(systemName: "square.and.pencil")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
Text("Some kind of title") // <- aligned properly
.fontWeight(.bold)
.minimumScaleFactor(0.2)
} //: HStack
.padding(.horizontal, 12)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: 35)
.font(.headline)
.foregroundColor(Color.green)
.background(Color.green.opacity(0.2))
.cornerRadius(6)
Button(action: {
// some action
}){
HStack{
Image(systemName: "square.and.pencil")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
Text("Some kind of title") // <- not aligned properly
.fontWeight(.bold)
.minimumScaleFactor(0.2)
} //: HStack
.padding(.horizontal, 12)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: 35)
.font(.headline)
.foregroundColor(Color.green)
.background(Color.green.opacity(0.2))
.cornerRadius(6)
} //: Button
} //: HStack

You can fix it by adding the multilineTextAlignment modifier to the Text view:
Button(action: {
// some action
}){
HStack{
Image(systemName: "square.and.pencil")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
Text("Some kind of title")
.fontWeight(.bold)
.minimumScaleFactor(0.2)
.multilineTextAlignment(.leading) // <- will align to the left
} //: HStack
.padding(.horizontal, 12)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: 35)
.font(.headline)
.foregroundColor(Color.green)
.background(Color.green.opacity(0.2))
.cornerRadius(6)
} //: Button
As to why it happens, I have a theory. By default Text on buttons is centered, while plain Text by default is left-aligned. I didn't see it explicitly mentioned in the documentation, but a simple experiment shows this behavior:

Related

HStack background color blending into safe area

I am trying to prevent the background of my HStack from going into my safearea.
There is a ZStack that is setting the background color. I am using green just to be more defined for now.
struct AccountView: View {
var body: some View {
ZStack{
Color(.green)
.ignoresSafeArea()
VStack(spacing: 32){
HStack {
Image(systemName: "person")
.resizable()
.scaledToFill()
.frame(width: 64, height: 64)
.clipShape(Circle())
.padding(.leading)
VStack(alignment: .leading, spacing: 4) {
Text("First Last")
.font(.system(size: 18))
Text("Available")
.foregroundColor(.gray)
.font(.system(size: 14))
}
Spacer()
}
.frame(height: 80)
.background(Color("ItemBG"))
Spacer()
}
}
}
}
We need to use shape as background instead of pure-colour, then it is not spread outside.
Here is simplified demo. Tested with Xcode 13.2 / iOS 15.2
var body: some View {
VStack {
HStack {
Text("Hello")
}
.frame(maxWidth: .infinity, maxHeight: 80)
.background(Rectangle().fill(.yellow)) // << here !!
Spacer()
}
.background(Color.green.ignoresSafeArea())
}
I was able to figure it out...
I had to give by HStack a padding of 1 on the top. Apparently with IOS 15 if it touches the safe area it bleeds.
HStack {
Image(systemName: "person")
.resizable()
.scaledToFill()
.frame(width: 64, height: 64)
.clipShape(Circle())
.padding(.leading)
VStack(alignment: .leading, spacing: 4) {
Text("First Last")
.font(.system(size: 18))
Text("Available")
.foregroundColor(.gray)
.font(.system(size: 14))
}
Spacer()
}
.frame(height: 80)
.background(Color("ItemBG"))
.padding(.top, 1)

ScrollView text alignment SwiftUI

I have a vertical ScrollView with a ForEach loop that has HStack's inside with Text, Image, and Two Text's. I'd like to keep each row's items in alignment with each other. I've attempted to wrap in a VStack with alignment .leading but did nothing. The issue I'm having is the text doesn't line up. Still very new to SwiftUI so any help would be appreciated.
ScrollView(.vertical) {
ForEach(self.daily.forecast, id: \.date) { forecast in
HStack (spacing : 10){
Text(forecast.date ?? "")
.fontWeight(.bold)
Spacer()
RequestImage(Url("IMAGE_URL"))
.aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30)
.clipped()
Spacer()
Text("\(forecast.maxTemp)")
.fontWeight(.bold)
Text("\(forecast.minTemp)")
.fontWeight(.bold)
}
}
}
.padding(.all, 15)
.onAppear() {
authorize {_ in
loadForecast()
}
}
UPDATED:
I was able to get a bit closer but things still aren't aligned to where I'd like them to be.
ScrollView(.vertical) {
ForEach(self.daily.forecast, id: \.date) { forecast in
HStack (spacing : 10){
Text(date)
.font(.title3)
.fontWeight(.bold)
.frame(minWidth: 45)
Spacer()
RequestImage(Url("IMAGE_URL"))
.aspectRatio(contentMode: .fit)
.frame(width: 35, height: 35)
.frame(minWidth: 50)
.clipped()
Spacer()
Text(maxTemp)
.font(.title3)
.fontWeight(.bold)
.frame(minWidth: 45)
Text(minTemp)
.font(.title3)
.fontWeight(.bold)
.foregroundColor(Color.secondary)
.frame(minWidth: 45)
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
}
.padding(.all, 15)
.onAppear() {
authorize {_ in
loadForecast()
}
}
Here is a demo of possible solution. Prepared with Xcode 12.4 / iOS 14.4
struct DemoWeatherLayout: View {
private var dayOfTheWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sut"]
var body: some View {
ScrollView {
VStack {
ForEach(dayOfTheWeek, id: \.self) {
WeatherRowView(day: $0,
icon: ["sun.max", "cloud.sun", "cloud", "cloud.snow"].randomElement()!,
low: Array(-5..<0).randomElement()!,
high: Array(1..<15).randomElement()!)
}
}.padding()
}
}
}
struct WeatherRowView: View {
var day: String
var icon: String
var low: Int
var high: Int
var body: some View {
HStack {
Text(day)
.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: icon)
.frame(maxWidth: .infinity, alignment: .leading)
HStack {
Text("+XXº").foregroundColor(.clear)
.overlay(Text(String(format: "%+d", low)), alignment: .leading)
Text("+XXº").foregroundColor(.clear)
.overlay(Text(String(format: "%+d", high)), alignment: .leading)
}
}
}
}
Three suggestions:
add alignment parameter to HStack:
HStack(alignment: .lastTextBaseline, spacing: 10) { ... //others options: .top, .center, .firstTextBaseline...
Add minLength parameter to Spacer:
Spacer(minLength: 0) but with spacing: 10 in HStack it is a bit redundant. You can remove spacing: 10 for Spacer(minLength: 10)
Add a Spacer(minLength: 0) at the end after Text("\(forecast.minTemp)").fontWeight(.bold)
I'm going to accept #Asperi answer as correct. The UI has indeed changed since to better fit said labels etc. Appreciate everyone's else. The code is very much ugly but works. Not sure if it's 100% correct.
HStack (spacing: 10){
Text(date)
.font(.title3)
.fontWeight(.bold)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
Spacer()
RequestImage(Url("IMAGE_URL_HERE"))
.aspectRatio(contentMode: .fit)
.frame(width: 35, height: 35)
.frame(minWidth: 55)
.clipped()
Spacer()
Text(maxTemp)
.font(.title3)
.fontWeight(.bold)
.frame(minWidth: 50)
.background(Color.orange)
.cornerRadius(5)
Text(minTemp)
.font(.title3)
.fontWeight(.bold)
.frame(minWidth: 50)
.background(Color.blue)
.cornerRadius(5)
}

SwiftUI WidgetKit text and image alignment

I'm attempting to make a widget extension with two text labels and an image. The two labels need to be in the top left and the image in the bottom right. I've been unable to get this working correctly.
Is there a proper way of doing this without having to use a Spacer() or an overlay image which isn't what I need. Very new to SwiftUI. Any help or pointers would be greatly appreciated.
VStack (alignment: .leading){
Text("Title")
.font(.headline)
.fontWeight(.regular)
.lineLimit(1)
Text("subtitle")
.font(.title3)
.fontWeight(.bold)
.lineLimit(1)
.allowsTightening(true)
HStack {
Spacer(minLength: 15)
Image("fish")
.resizable()
.frame(width: 120, height: 80)
}
}
.padding(.top)
.padding(.leading)
There are a bunch of ways to build a layout like that. What you have is almost working, you just need a Spacer between the text and image to shove the image down:
VStack (alignment: .leading) {
Text("Title")
.font(.headline)
.fontWeight(.regular)
.lineLimit(1)
Text("subtitle")
.font(.title3)
.fontWeight(.bold)
.lineLimit(1)
.allowsTightening(true)
Spacer(minLength: 0) // <-- add spacer here
HStack {
Spacer(minLength: 15)
Image("fish")
.resizable()
.frame(width: 120, height: 80)
}
}
.padding(.top)
.padding(.leading)
The only other change I would suggest is avoiding the hard-coded image size, since widgets are different sizes on different phones. You could have the image expand to fit the available width while preserving your image’s 3:2 aspect ratio like this:
VStack(alignment: .leading) {
Text("Title")
.font(.headline)
.fontWeight(.regular)
Text("subtitle")
.font(.title3)
.fontWeight(.bold)
.allowsTightening(true)
Spacer(minLength: 0)
Image("fish")
.resizable()
.aspectRatio(3/2, contentMode: .fit)
}
.lineLimit(1)
.padding(.leading)
.padding(.top)
Or if you want the fixed-size image and would prefer to have the text overlap it on smaller screens, you could use a ZStack like this:
ZStack(alignment: .bottomTrailing) {
Image("fish")
.resizable()
.frame(width: 120, height: 80)
VStack (alignment: .leading) {
Text("Title")
.font(.headline)
.fontWeight(.regular)
Text("subtitle")
.font(.title3)
.fontWeight(.bold)
.allowsTightening(true)
}
.lineLimit(1)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
As I said, there are a bunch of ways to solve this!
I think I've managed to get this working, well as close as I could get it. Seems to be a bit better than before. Mildly satisfied but working now.
ZStack {
VStack (alignment: .leading){
Text("Title")
.font(.body)
.fontWeight(.bold)
.lineLimit(1)
.allowsTightening(true)
Text("subtitle")
.font(.title)
.fontWeight(.regular)
HStack(alignment: .bottom){
Spacer()
Image("fish")
.resizable()
.frame(width: 120, height: 80)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
.offset(x: 5, y: -10)
// padding for images with frame bigger than actual image
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .leading)
}
.padding(12)
}
.background(Color(UIColor.secondarySystemBackground))

SwiftUI: How to restrict view to only resizing in one direction?

How can I restrict the view to resize only downwards?
I am trying to add more buttons & have the view resize to accommodate the buttons without overlapping the red rectangle above.
This is the code I am currently using:
struct ContentView: View {
var body: some View {
VStack{
RoundedRectangle(cornerRadius: 20)
.frame(width: .infinity, height: 55)
.foregroundColor(.red)
HStack{
Spacer()
ZStack{
RoundedRectangle(cornerRadius: 20)
.foregroundColor(.green)
VStack{
Button(action: {}) {
Text("Test Button")
.foregroundColor(.white)
.font(.headline)
.frame(width: .infinity)
Spacer()
}.padding()
Button(action: {}) {
Text("Test Button")
.foregroundColor(.white)
.font(.headline)
.frame(width: .infinity)
Spacer()
}.padding()
Button(action: {}) {
Text("Test Button")
.foregroundColor(.white)
.font(.headline)
.frame(width: .infinity)
Spacer()
}.padding()
}
}.frame(width: 250, height: 100)
}
Spacer()
}
}
}
Change your .frame(width: 250, height: 100) to .frame(width: 250, height: 100, alignment: .top) Your original example defaults to .center, which is why it expands into the red rectangle.

swiftui edgesIgnoreSafeArea only showing on canvas and not working on actual device

Im using .edgesIgnoringSafeArea(.top) to ignore the safe area on the top. this seems to work, and show the image going over the safe area on the top as expected on the canvas. But when I run this on a iPhone 11 plus simulator or iPhone x device (i also tested on other devices on the simulator) it doesn't seem to ignore the edges.
I have tried excluding:
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
But I need the above so I can use a custom back button.
var body: some View {
VStack (alignment: .leading ) {
Image("user1")
.resizable()
.frame(width: (UIScreen.main.bounds.width), height: ((UIScreen.main.bounds.height) / 2) + 50)
ScrollView {
HStack {
Text("\(userData.username), 30")
.fontWeight(.semibold)
.font(.system(.largeTitle, design: .rounded))
.padding([.leading,.top])
Spacer()
Image(systemName: "paperplane")
.aspectRatio(contentMode: .fill)
.frame(width: 65, height: 65)
.background(Color("message-background"))
.foregroundColor(.white)
.cornerRadius(25)
.font(.system(size: 25))
.padding(.top)
}.padding(.trailing)
HStack {
Text("Singer, London")
.fontWeight(.light)
.padding([.leading])
Spacer()
}
HStack {
Text("all the bio info will go there all the bio info will go thereall the bio info will go thereall the bio info will go there all the bio info will go there.")
.fontWeight(.light)
.padding([.leading])
.padding(.top)
.frame(width: (UIScreen.main.bounds.width - 20))
Spacer()
}.padding(.bottom,50)
}
}.edgesIgnoringSafeArea(.top)
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}){
Image(systemName: "arrow.left")
.aspectRatio(contentMode: .fill)
.frame(width: 40, height: 40)
.background(Color("message-background"))
.foregroundColor(.white)
.cornerRadius(15)
.font(.system(size: 15))
.padding(.top,20)
})
}
Your code works for me on the simulator as expected. I just added the Text "This works" to demonstrate it:
So your problem must be somewhere outside the code which you presented in your question.