SwiftUI - Vertical Centering Content inside Scrollview - ios

i'm trying to code a simple login page on my app. I started using SwiftUI on my newlly updated Mac OS Catalina. The Apple documentation is still lacking a lot.
I need to center a VStack vertically on a Scrollview ocupying the whole page with a "limit" on it's width of 400.
Something like this:
ScrollView(.vertical) {
VStack {
Text("Hello World")
}
.frame(maxWidth: 400, alignment: .center)
}
It was easy with UIScrollView, just needed to set the ContentView to fill height and width and then centering a Vertical StackLayout inside the Content View but now with SwiftUI i just wonder..
The goal is something like this (Credit to the author)
If someone is wondering why i want everything inside a scrollview, it's beacause my form is quite big and i expect the user to use both landscape and portrait view so i really need the content to be scrollable, bear in mind also that in a Ipad the form doens't fill the whole screen that's why i want it centered vertically.

You can vertically center content in a scroll view by using GeometryReader to get the parent view's dimensions and setting the scroll view's content's minHeight to the parent's height.
When the content is too big to fit vertically it'll just scroll like normal.
For example:
var body: some View {
GeometryReader { geometry in // Get the geometry
ScrollView(.vertical) {
VStack {
Text("Form goes here")
.frame(maxWidth: 400) // Set your max width
}
.padding()
.background(Color.yellow)
.frame(width: geometry.size.width) // Make the scroll view full-width
.frame(minHeight: geometry.size.height) // Set the content’s min height to the parent
}
}
}

I've build a more generic view based on #Alex answer
/// Custom vertical scroll view with centered content vertically
///
struct VScrollView<Content>: View where Content: View {
#ViewBuilder let content: Content
var body: some View {
GeometryReader { geometry in
ScrollView(.vertical) {
content
.frame(width: geometry.size.width)
.frame(minHeight: geometry.size.height)
}
}
}
}
You can use it anywhere in your app like this
var body: some View {
VScrollView {
VStack {
Text("YOUR TEXT HERE")
}
}
}

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.

Match UIStackView's .fill alignment with SwiftUI VStack

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:

Update SwiftUI View size to match image

I'm trying to make a view which holds an image loaded asynchronously from a network request. Before the image loads, I want to show a placeholder view which has a fixed size. When the image loads, I want to replace this placeholder view with the image, scaled to fit inside the frame of the placeholder view, but then I want the parent view to shrink to match the size of this image. I can't figure out how to do this last part.
Right now, it looks like this:
struct ItemCell: View {
var body: some View {
Group {
CustomImageView(from: imageURL, placeholder: PlaceholderView(), config: { $0.resizable() })
.aspectRatio(contentMode: .fit)
.frame(minWidth: 0, maxWidth: 150, minHeight: 0, maxHeight: 190, alignment: .bottomLeading)
}.background(Color.red) // To show that the view isn't resizing properly
}
}
struct PlaceholderView: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 3, style: .continuous)
.frame(width: 150, height: 190)
.foregroundColor(Color(.secondarySystemBackground))
Image(systemName: "globe")
.resizable()
.scaledToFit()
.frame(width: 50)
.foregroundColor(.secondary)
}
}
}
The CustomImageView is adapted from this article on loading images asynchronously. The ItemCells are placed in a horizontal ScrollView. When I test this, it:
correctly displays the placeholder view before the image is loaded;
resizes the image so it maintains its aspect ratio and fits inside the 150x190 frame, but has a weird animation where some of the images shrink and then expand back; also, some of the images seem to shrink too much;
does not resize the parent view to match the size of the image properly, but instead retains the full original height and some (?) extra width on some cells.
These two problems are shown in the gif below, with blue images and a red background. Notice the extra height on the first and third cells, and the extra width on the second. Also, note that the first image ends up smaller than when it first loads, even though it fit inside the original 150x190 frame at first.
How can I fix these problems?
Figured out how to do it. There were several problems with my original code. First, the ItemCells used in the ScrollView should be modified with the .fixedSize() view modifier, like so:
ScrollView(...) {
HStack(...) {
ForEach(...) { ...
ItemCell()
.fixedSize()
}
}
}
Then, changing the frame of the CustomImageCell to be use idealHeight instead of maxHeight and making the Group a VStack with a Spacer() to push everything to the bottom, as #Paulw11 had suggested in comments:
struct ItemCell: View {
var body: some View {
VStack {
Spacer()
CustomImageView(from: imageURL, placeholder: PlaceholderView(), config: { $0.resizable() })
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 150, idealHeight: 190)
}
}
}
These changes fix both the image resizing animation issue and the extra space issue.

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)

How do I pin a view to the edges of a screen?

How do I pin a view (in this case, a label/text) to an edge of a screen with SwiftUI? With Storyboards I would just use AutoLayout but that isn't available with SwiftUI.
You can do something like this
VStack {
HStack {
Text("Label")
Spacer()
}
Spacer()
}
Spacer in VStack will make sure HStack is at the top, Spacer in HStack will make sure Text is all they way to the left. You can also solve this with alignments.
You can wrap your main content in a special container called GeometryReader. Its default size is the same as its parent so if it is the root view it will pin the contents to the screen edges like AutoLayout.
GeometryReader { in geometry
YourContentView().frame(width: geometry.size.width, height: geometry.size.height)
}
In case anyone is here to find a way to pin a view x points from the top, you can do this:
VStack {
Spacer()
.frame(height: 40)
Text("Space me")
Spacer()
}
You'll need both spacers. This can be a bit counter-intuitive if you're coming from Auto Layout but it's actually quite convenient. The spacer at the bottom of your VStack will "push" your VStack views from the default (i.e. centered) y position toward the top until it meets resistance – by default the top edge of the safe area. You can then push the resting point down by x points with the top spacer, giving a similar effect as Auto Layout's top constraint.
Use this modifier on your view
.frame(minWidth: 0, maxWidth: .infinity,
minHeight: 0, maxHeight: .infinity)
SwiftUI uses minimum needed space for any view, to increase it you may use different approaches depending on the situation.
Also it's possible to use relativeSize() modifier, but I didn't get yet when it works

Resources