SwiftUI: How to know correct order of modifiers? - ios

I'm pretty new to SwiftUI, learning it for the first time, and couldn’t understand why the below snippet doesn’t work. Ideally, the VStack should stretch in all directions and the Image should have a width of 200px without losing its aspect ratio.
Code
struct ContentView: View {
var body: some View {
VStack() {
Image("Image Name")
.resizable()
.frame(width: 200)
.aspectRatio(contentMode: .fit)
}
.background(Color.red)
.frame(maxWidth: .infinity,maxHeight: .infinity)
}
}
After I accidentally reordered the modifiers, it worked. So, how am I supposed to know the correct order of modifiers without a hit and trial method each time?
// new VStack modifier order
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
// new Image modifier order
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)

The best way to think about it for now is to imagine that SwiftUI renders your view after every single modifier. So, as soon as you say .background(Color.red) it colors the background in red, regardless of what frame you give it. If you then later expand the frame, it won’t magically redraw the background – that was already applied.
Of course, this isn’t actually how SwiftUI works, because if it did it would be a performance nightmare, but it’s a neat mental shortcut to use while you’re learning.
Please refer to this link for more details https://www.hackingwithswift.com/books/ios-swiftui/why-modifier-order-matters#:~:text=Every%20time%20we%20modify%20a,up%3A%20ModifiedContent%3CModifiedContent%3C%E2%80%A6

Related

SwiftUI - can I make one element in Form fill the whole screen width (without horizontal margins)?

I would like a single item inside SwiftUI Form to run from side to side, without having Form's default margins.
Unfortunately, whatever I do (like ading a wider .frame, negative horizontal padding, or .offset), the team image view seems to be always cropped by the form to fit the form area (= has horizontal margins).
Is it possible to make the Image below touch the left and right side of the screen?
I am using Form for my app settings, but I would like to add a full-width section there (think eg. a banner to promote a feature).
SwiftUI Playgrounds code:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
var body: some View {
Form {
Section(
header: Text("First section")
) {
Text("Hello world")
}
Text("The image below should be stretched to touch the left and right window edge, without being cropped by the Form.")
Image(systemName: "sun.max.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.listRowInsets(EdgeInsets()) // this is supposed to fix the problem, but all it does is to set the default item inner padding to zero, so the image at least touches the edge of teal area.
.listRowBackground(Color.teal)
Section(
header: Text("Last section")
) {
Text("Hello world")
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
How it looks:
Unfortunately, SwiftUI Form is very temperamental and forces you to strictly adhere to the standard iOS Settings screen formatting.
Fortunately, you can re-implement similar formatting yourself with SwiftUI!
For the top, something like:
VStack(spacing: 4) {
Text("FIRST SECTION")
.font(.system(size: 12, weight: .regular))
.foregroundColor(.gray)
.padding(.leading)
Text("Hello, world!")
.font(.system(size: 15, weight: .regular))
.foregroundColor(.black)
.padding(.horizontal)
.frame(height: 44, alignment: .center)
.background(Color.white)
.cornerRadius(10)
}

`maxHeight` behaves exactly as `height` frame modifier

I'm confused by frame(maxHeight: ...) modifier. I would expect that the resulting view would have dynamic height capped at maxHeight. However, the height of the view is always maxHeight.
In the example below, I wanted the green rectangle to have 30px and the red one should fill the rest space. However, there's additional padding between the rectangles caused by maxHeight modifier.
Is there any other way to achieve what I want?
maxHeight seems useless to me know. It's pretty much the same as using height, isn't it?
Code
var body: some View {
VStack(spacing: 0) {
Color.green
.frame(height: 30)
.frame(maxHeight: 60, alignment: .top)
Color.red
}
.frame(height: 100)
.background(Color.black)
}
Preview:
Every modifier can (in general!) create a new view. So, you create at first green view unlimited, then limit it by 30px (so green filled that space), and then create another view with 60px (and because there is no stopper it filled that available space)... and then everything with red view below(!) considered above views. To get what you expected just remove .frame(maxHeight - it is not needed here.
... but, if you still want (for any reason) to keep it and fulfil expectation, then it is a matter for layout order - give a preference to second view, like
VStack(spacing: 0) {
Color.green
.frame(height: 30)
.frame(maxHeight: 60, alignment: .top)
Color.red
.layoutPriority(1) // << here !!
}
and you get

SwiftUI - corner radius changes the height of an image

If the .cornerRadius modifier comes after the .frame modifier, the image becomes much slower. What is the reason behind this ?
struct ContentView: View {
var body: some View {
VStack(spacing: 100) {
Image("image1")
.resizable()
.scaledToFill()
.frame(width: 343, height: 184)
.cornerRadius(8)
Image("image1")
.resizable()
.scaledToFill()
.cornerRadius(8)
.frame(width: 343, height: 184)
}
}
}
In SwiftUI the order of modifiers is important and can lead to a different output.
A rule of thumb is:
read them from bottom to top
understand that every modifier produce a new view
the modifier "modifies" only what's below:
-> .frame(width: 343, height: 184)
-> .cornerRadius(8)
-> .scaledToFill()
-> .resizable()
Since you used the cornerRadius the documentation states that:
Clips this view to its bounding frame, with the specified corner
radius.
Behind the scene, you can imagine the engine applying two modifiers, the clipped and the corner radius.
Because every modifier produces a new view when the clipping is calculated it's like it doesn't know what are the bounds in which constrain the new view and that explains why the image goes out.
If we go a step further where we try to apply two frame and cornerRadius hopefully should be clearer how SwiftUI interprets the modifiers:
If you want to know more, this discussion goes deeper into the details of how SwiftUI sees the modifier: Order of modifiers in SwiftUI view impacts view appearance

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 text-alignment

Among the many properties of the Text view, I couldn't find any related to text alignment. I've seen in a demo that it automatically handles RTL, and when placing stuff using View's body, it always centers it automatically.
Is there some concept that I'm missing about layout system in SwiftUI and if not, how can I set the text alignment properties to the Text?
You can do this via the modifier .multilineTextAlignment(.center).
Text("CENTER")
.multilineTextAlignment(.center)
Apple Documentation
From SwiftUI beta 3 forward, you can center a text view with the frame modifier:
Text("Centered")
.frame(maxWidth: .infinity, alignment: .center)
Was trying to understand this myself as other answers here mention Text.multilineTextAlignment(_:) / VStack(alignment:) / frame(width:alignment:) but each solution solves a specific problem. Eventually it depends on the UI requirement and a combination of these.
VStack(alignment:)
The alignment here is for the inner views in respective to one another.
So specifying .leading would associate all inner views to have their leading aligned with one another.
VStack(alignment: .leading, spacing: 6) {
Text("Lorem ipsum dolor")
.background(Color.gray.opacity(0.2))
Text("sit amet")
.background(Color.gray.opacity(0.2))
}
.background(Color.gray.opacity(0.1))
.frame
In frame(width:alignment:) or frame(maxWidth:alignment:), the alignment is for the contents within the given width.
VStack(alignment: .leading, spacing: 6) {
Text("Lorem ipsum dolor")
.background(Color.gray.opacity(0.2))
Text("sit amet")
.background(Color.gray.opacity(0.2))
}
.frame(width: 380, alignment: .trailing)
.background(Color.gray.opacity(0.1))
The inners views are leading aligned respective to one another but the views themselves are trailing aligned respective to the VStack.
.multilineTextAlignment
This specifies the alignment of the text inside and can be seen best when there are multiple lines otherwise without a defined frame(width:alignment), the width is automatically adjusted and gets affected by the default alignments.
VStack(alignment: .trailing, spacing: 6) {
Text("0. automatic frame\n+ view at parent's specified alignment\n+ multilineTA not set by default at leading")
.background(Color.gray.opacity(0.2))
Text("1. automatic frame\n+ view at parent's specified alignment\n+ multilineTA set to center")
.multilineTextAlignment(.center)
.background(Color.gray.opacity(0.2))
Text("2. automatic frame\n+ view at parent's specified alignment\n+ multilineTA set to trailing")
.multilineTextAlignment(.trailing)
.background(Color.gray.opacity(0.2))
}
.frame(width: 380, alignment: .trailing)
.background(Color.gray.opacity(0.1))
Tests with combinations:
VStack(alignment: .trailing, spacing: 6) {
Text("1. automatic frame, at parent's alignment")
.background(Color.gray.opacity(0.2))
Text("2. given full width & leading alignment\n+ multilineTA at default leading")
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.2))
Text("3. given full width & center alignment\n+ multilineTA at default leading")
.frame(maxWidth: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
Text("4. given full width & center alignment\n+ multilineTA set to center")
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
Text("5. given full width & center alignment\n+ multilineTA set to trailing")
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
Text("6. given full width but no alignment\n+ multilineTA at default leading\n+ leading is based on content, looks odd sometimes as seen here")
.frame(maxWidth: .infinity)
.background(Color.gray.opacity(0.2))
}
.frame(width: 380)
.background(Color.gray.opacity(0.1))
I've actually run into the problem where I had to align text on a single line. What I've found to work is this:
Text("some text")
.frame(alignment: .leading)
If you combine this with the frame width parameter you can get some nice text block formatting for labels and such.
I guess SwiftUI wants us to use wrappers like stacks for such things.
So instead of writing something like Text("Hello World").aligned(.leading), the following is encouraged:
VStack(alignment: .leading) {
Text("Hello World")
}
We need to align the Text and not the Stack it's in. So calling multilineTextAlignment(.center) and setting the line limits I can be able to see the texts aligned to center. I don't know why I have to set the line limits, I thought it would expand if you have a large text.
Text("blahblah")
.font(.headline)
.multilineTextAlignment(.center)
.lineLimit(50)
If you would like to keep constant width for the Text, the ".multilineTextAlignment(.leading)" won't take any effect until there is only one line of text.
This is the solution that worked for me:
struct LeftAligned: ViewModifier {
func body(content: Content) -> some View {
HStack {
content
Spacer()
}
}
}
extension View {
func leftAligned() -> some View {
return self.modifier(LeftAligned())
}
}
Usage:
Text("Hello").leftAligned().frame(width: 300)
I had the same problem.
i used this for fixing that.
Text("Test")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
You can set alignment for Vertical stackView as leading. Like below
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
Text("Joshua Tree National Park")
.font(.subheadline)
}
I'd like to use Spacer() view to aligning text block.
This example show text at the trailing side:
HStack{
Spacer()
Text("Wishlist")
}
You can always add a frame to the Text field and can modify it's alignment.
Text("Hello World!")
.frame(alignment : .topLeading)
Since, this is just for a couple of lines - this is better than using alignment on either of the Stacks
Not sure if this is the answer you are looking for but I have experienced that SwiftUI automatically switches to RTL for languages like Arabic, you don't need to explicitly specify that like in UIKit.
You can use this property of SwiftUI
multilineTextAlignment
for TextAlignment.
VStack {
Text("Your Text")
.multilineTextAlignment(.center)
}

Resources