Match UIStackView's .fill alignment with SwiftUI VStack - ios

I've got a VStack with 3 Texts in it. Each has a different length string. I'd like the VStack's width to be just big enough to fit the widest Text, but I'd also like all three Texts to fill the VStack horizontally.
By default, with this code:
VStack(spacing: 4.0) {
ForEach(1..<4) {
Text(Array<String>(repeating: "word", count: $0).joined(separator: " "))
.background(Color.gray)
}
}
I get:
I want:
In UIKit, I could do this with a vertical UIStackView whose alignment property was set to .fill. VStack doesn't have a .fill alignment. The suggested solution I've seen is to modify the frame of each child of the stack view (ie. each Text) with .infinity for maxWidth.
The suggestion I've found is to modify the Texts with .frame(maxWidth: .infinity). However, this makes the whole VStack expand to (presumably) its maximum size:
Is there a way to make the VStack naturally grow to the size of its widest child, and no larger, while making all its children the same width?

Just add .fixedSize().
struct ContentView: View {
var body: some View {
VStack(spacing: 4.0) {
ForEach(1..<4) {
Text(Array<String>(repeating: "word", count: $0).joined(separator: " "))
.frame(maxWidth: .infinity) /// keep the maxWidth
.background(Color.gray)
}
}
.fixedSize() /// here!
}
}
Result:

Related

SwiftUI how to align the view‘s leading to the most super view’s leading?

I’m new to SwiftUI and I’m making a widget. The default code included a text view which is both x-centered and y-centered in the super view(which I don’t know if there’s the same concept in SwiftUI).
This is my code:
struct WidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack(alignment: .leading){
Text(Date(), style: .time)
.padding(.leading)
}
}
}
I want to align the view’s leading to the super view’s leading instead of positioning the text in the center. So I tried to add a padding to the text’s leading, strangely, it seems that the text view is not a direct subview of the superview, it is positioned in a invisible centered view instead, since I end up with having a text view that slightly deviate the center, looks like this:
This is what I want:
I tried the position method and it was the only one worked, not perfectly though, since it is based on the center point.
I’m looking for a better solution.
Here is a Playground that gives an example of a view that looks like your picture:
import UIKit
import SwiftUI
import PlaygroundSupport
struct ViewWithText : View {
var body : some View {
HStack {
Text("Here")
.background(Color.yellow)
}
.frame(width: 320, height: 480, alignment: .leading)
.background(Color.gray)
}
}
let hostingController = UIHostingController(rootView: ViewWithText())
PlaygroundSupport.PlaygroundPage.current.liveView = hostingController
With simple padding and background you can see why it does not work for you as you want.
VStack(alignment: .leading){
Text(Date(), style: .time)
.padding(.leading)
.background(.yellow)
}
.padding()
.background(Color.gray)
The base size of the VStack is the same as the size of it's children. Solution would be to increase the size of the VStack or the size of the Text
In widget I suggest to set the parent (V)Stack's size (maxWidth) as big as it can get (.infinity) :
VStack(alignment: .leading){
Text(Date(), style: .time)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
Btw setting padding(.leading) is causing the "text view that slightly deviate the center" as this added a base padding to the left size of the Text. It just increases the width of the text, but still not fully to the available width.

Text in a VStack being condensed

I have a view similar to the below code. For some reason, my long text is being condensed, when it shouldn't be. Why is this?
struct ContentView: View {
var body: some View {
VStack {
ScrollView {
VStack(alignment: .leading, spacing: 32) {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "hand.raised.fill")
.resizable()
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text(shortText)
Text(longDescription)
.lineSpacing(4)
// this is being condensed
}
}
}
.padding([.leading, .trailing])
}
}
VStack {
// Custom Button goes here
Spacer()
}
.frame(height: 70)
}
}
.layoutPriority(1) tells the layout system that it should give priority to the desired dimensions of this view over anything with a lower priority, so it won't be condensed.
The reason your Text is being condensed is because it's in a ScrollView.
In a non-scrolling context, the layout system allows the views to expand, using the available space (i.e. the screen size) as the limit.
In a scrolling context there is no limit to the available space, so allowing views to expand to their maximum size could result in some views having an infinite size (Color.red, for example, will try to take as much space as possible).
This is why your Text view is condensed to the minimum space it supports (one line height) as opposed to the maximum size it supports.
PS. There might be a combination of modifiers that is not layoutPriority(1) that allows for Text to report it's minimum size as the required size to show all of its contents. If I have time to try and find it I will update this answer.

When there are different sized Text in a HStack, top alignment doesn’t apply to larger sized text

I have a HStack with multiple elements, particularly two Texts with different font sizes. I want both text to be aligned to the top of the view.
HStack(alignment: .top) {
Image(systemName: "cloud.drizzle.fill")
Text("14°")
.font(.largeTitle)
Text("86%")
.font(.callout)
Spacer()
}
However, the first (larger) Text is outputted below the other two:
Actually it's aligned correctly , add backgrounds to each Text and you will find that the frame of the Text is aligned correctly
but to solve the case that you are looking for , I did a hack for you , by doing some calculs
The result:
1) Alignement of the two Text
Put both of them in one HStack , with alignment: .firstTextBaseline
Then play on the second text , by adding a baselineOffset with (bigFont.capHeight - smallFont.capHeight)
You can learn more about fonts , but the main information that you need is this :
So your code will be :
HStack(alignment: .firstTextBaseline) {
Text("14°")
.font(Font(bigFont))
.background(Color.blue)
Text("86%")
.font(Font(smallFont))
.baselineOffset((bigFont.capHeight - smallFont.capHeight))
.background(Color.yellow)
Spacer()
}
2) Align the Image with the text :
by adding a padding which will be equal to bigFont.lineHeight-bigFont.ascender (go back to the picture on top , to see how I calculated it )
And the final code :
struct ContentView: View {
#State var pickerSelection = ""
let bigFont = UIFont.systemFont(ofSize: 50)
let smallFont = UIFont.systemFont(ofSize: 15)
var body: some View {
HStack(alignment:.top) {
Image(systemName: "cloud.drizzle.fill")
.background(Color.red)
.padding(.top, bigFont.lineHeight-bigFont.ascender)
HStack(alignment: .firstTextBaseline) {
Text("14°")
.font(Font(bigFont))
.background(Color.blue)
Text("86%")
.font(Font(smallFont))
.baselineOffset((bigFont.capHeight - smallFont.capHeight))
.background(Color.yellow)
Spacer()
}
}
}
}
PS : I added backgrounds to show you the real frames of each view
Currently the texts are aligned by top. but the large text has ascent height that is larger than small text. so the align is not top of text.
Unfortunately, SwiftUI doesn't support the alignment of top of text.
But you can align the top of text manually like as following code.
HStack(alignment: .top) {
Image(systemName: "cloud.drizzle.fill")
Text("14°")
.font(.largeTitle).padding(.top, -5.0)
Text("86%")
.font(.callout)
Spacer()
}

SwiftUI: Can't align image to the top of ZStack

I can't figure out how to align Image view on top of ZStack, by default views in SwiftUI are placed at the center of their parent, and we then use stacks to align them, I have the following piece of code:
struct ContentView: View {
var body: some View {
ZStack {
Image("bgImage")
Text("Hello, World!")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red) //this is for debugging purposes, to show the area of the ZStack
}
}
How can I position the image to the top ?
To tell the ZStack to align things a particular way within it, configure it with the alignment parameter:
ZStack(alignment: .top) {
Color.clear
Image(...)
Text("Hello, World!")
}
(Color.clear expands to fill all available space, so this forces your ZStack to be as large as the enclosing view without needing to add a .frame().)
That will align everything at the top of course, which might not be what you want. You could fix that by making a nesting your ZStacks to align as you want them to:
ZStack{
ZStack(alignment: .top) {
Color.clear
Image(...) // This will be at the top
}
Text("Hello, World!") // This will be centered
}
That said, I'd probably use a .background for this example.
ZStack {
Color.clear
Text("Hello, World!")
}
.background(Image(...), alignment: .top)
And if you only have one view, you can get rid of the ZStack and use a frame instead:
Text("Hello, World!")
.frame(maxHeight: .infinity)
.background(Image(uiImage:#imageLiteral(resourceName: "image.jpg")),
alignment: .top)
Keep in mind that in this case the image will draw outside its frame. In many cases that's fine (and it's completely legal), but it can matter sometimes (for example, if you put this inside a stack). You can add .border(Color.green) to the end to see how that works.
This example really gets to the heart of SwiftUI layout, so it's worth understanding what's going on. This isn't a workaround or a trick, so you should get to the place where this feels very normal.
The top-level content view (the one that contains the ZStack) offers its entire space to the ZStack. A ZStack is always exactly the size that contains its contents, so first the ZStack needs to layout its children. It lays them out according to its alignment, and then sizes itself exactly to fit around them. So with top-alignment (but without Color.clear), the Image is at the top of the ZStack. The ZStack is just exactly the same size as the Image.
The top-level content view then places the ZStack in its center.
The way the ZStack lays out its children is similar to how the content view did. It offers all the space it was offered, and then the child-views decide their sizes. Views always decide their own sizes. The Image and Text are fixed-sized views, so they are just the size of their contents. But Color is a flexible-sized view. It accepts the entire space that the ZStack offered (which is the same space that the top-level content view offered) and returns that as its size. Since a ZStack must exactly contain its children, the ZStack is now the size of the top-level content view, and things behave as you expect, aligning at the top of the screen.
Let's compare to using .frame() as you originally did:
ZStack {
Image("bgImage")
Text("Hello, World!")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red) //this is for debugging purposes, to show the area of the ZStack
First, I want to focus on your comment, because it's not correct. This is not the area of the ZStack. The ZStack is exactly the size of its contents (the Image and the Text). You put the background on the frame view, and the frame view is larger.
A key confusion people have is that they think .frame(...) changes the size of the View it's attached to. That's not correct at all. As before, a ZStack is always the size of its contents. .frame() creates a completely new view of the requested size. It then positions the wrapped view inside itself according to the frame's alignment. So in this example it works like this:
Top-level - Background - Frame - ZStack - { Image Text }
The top-level view offers all its space to the Background. Backgrounds are the size of what they contain, so it offers all of that space to the Frame. The Frame is flexible in both directions (due to the max fields), and so it ignores its child's size and chooses to be the size it was offered.
The Frame then offers all that space to the ZStack. The ZStack lays out its children, and returns its size as exactly the size that contains them.
The Frame then places the ZStack according to the Frame's alignment (.center, since that's the default). If you'd set the Frame's alignment to .top, then the ZStack would have been placed at the top of the frame (but the text would be centered in the ZStack not in the Frame).
It then reports to the Background that it is as large as the top-level view (since its flexible).
The Background then claims that same size to the top-level content view.
And finally, the top-level content view places the Background in its center.
You could always just put the things you want to be at the top in a VStack and use a Spacer.
ZStack(){
Image(...)
Spacer()
}
The complete code should look something like this:
struct ContentView: View {
var body: some View {
ZStack(){
Text("Hello, World!")
VStack {
Image(...)
Spacer()
}
}
}
}
You could do this with HStacks as well. Important to notice that if the image has no limits to its size, it will always take up as much space as possible. That would remove the purpose of the Spacer. Hope this helps :-)
So one thing working against you is the infinity maxHeight modifier, assuming that you do not want some space between the image and the bottom of the view.
.edgesIgnoringSafeArea(.all)
You may just need to tell your ZStack to ignore safe area insets.
struct ContactsView: View {
var body: some View {
ZStack {
Image("bgImage")
Text("Hello, World!")
}.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.red).edgesIgnoringSafeArea(.all)
}
}
If you need space between the bottom and the image, wrap the ZStack in a VStack and throw a Spacer in the bottom of the VStack.
struct ContactsView: View {
var body: some View {
VStack {
ZStack {
Image(systemName: "bgImage")
Text("Hello, World!")
}.frame(maxWidth: .infinity, maxHeight: 300).background(Color.red)
Spacer()
}.edgesIgnoringSafeArea(.all)
}
}
ZStack {
Image("background")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
}
Add .edgesIgnoringSafeArea(.all)

SwiftUI: minimumScaleFactor not applying evenly to stack elements

I have a horizontal stack of two pieces of text (the second highlighted in a blue). It fits fine on an iPhone XR, however when on a smaller device (like iPhone X), the text doesn't fit. I attempted to solve this by using minimumScaleFactor to scale the text. However, SwiftUI seems to make decisions on what to scale in the stack. In this example, on the smaller device, it removes the bolding and shrinks the first (non-blue) element only. The blue element remains unchanged. Any ideas as to why this would happen? How can I scale both bolded text elements down in size together? Thanks!
var normalText: String
var highlightedText: String
var body: some View {
HStack() {
Text(normalText)
.font(.largeTitle)
.bold()
.lineLimit(1)
Text(highlightedText)
.font(.largeTitle)
.bold()
.lineLimit(1)
.foregroundColor(.blue)
Spacer()
}
.minimumScaleFactor(0.5)
}
}
Here is how it displays on a smaller device:
And how it shows on a larger device:
struct ContentView: View {
var normalText: String = "Hello and Welcome to Stack "
var highlightedText: String = "Overflow"
var body: some View {
HStack() {
Group {
Text(normalText).bold() +
Text(highlightedText).bold()
.foregroundColor(.blue)
}
.lineLimit(1).font(.largeTitle)
Spacer()
}
.minimumScaleFactor(0.5)
}
}
+ is defined for 2 Text's but not 2 View's and as lineLimit() returns a View the joined text's needed to be Grouped. I made the combined string longer to force it to shrink.

Resources