Center and align SwiftUI elements in a ScrollView - swift

I have the following SwiftUI view:
import SwiftUI
struct MySwiftUIView: View {
var body: some View {
VStack {
HStack {
Text("top leading text")
Spacer()
}
Spacer()
HStack {
Spacer()
Text("centered text")
Spacer()
}
Spacer()
HStack {
Spacer()
Text("bottom trailing text")
}
}
}
}
It looks like this when run:
If I embed the view in a ScrollView like this:
import SwiftUI
struct MySwiftUIView: View {
var body: some View {
ScrollView {
VStack {
HStack {
Text("top leading text")
Spacer()
}
Spacer()
HStack {
Spacer()
Text("centered text")
Spacer()
}
Spacer()
HStack {
Spacer()
Text("bottom trailing text")
}
}
}
}
}
Then it looks like this when run:
How do I make the centered text centered, and the bottom trailing text rest on the bottom, when they are embedded in a ScrollView?
In a way, I want to use SwiftUI to replicate this scrolling behaviour seen in Xcode's inspectors, where the text "Not Applicable" is centered and scrollable:

Wrap the ScrollView inside GeometryReader. Then apply a minimum height to the scroll view equal to the geometry.size.height. Now the spacers you applied should fill the VStack the way you intended them to. Try the code below:
GeometryReader { geometry in
ScrollView(.vertical) {
VStack {
HStack {
Text("top leading text")
Spacer()
}
Spacer()
HStack {
Text("centered text")
}
Spacer()
HStack(alignment: .bottom) {
Spacer()
Text("bottom trailing text")
}
}
.padding()
.frame(width: geometry.size.width)
.frame(minHeight: geometry.size.height)
}
}
Also check this post for more discussion on this issue: SwiftUI - Vertical Centering Content inside Scrollview

Related

SwiftUI TabView covers content view

Is it possible to have TabView tabItem to be positioned below the screen content?
My layout is:
var body: some View {
TabView {
self.contentView
.tabItem {
Image(systemName: "chart.bar.fill")
Text("Chart")
}
Text("Screen 2")
.tabItem {
Image(systemName: "person.3.fill")
Text("Leaderboard")
}
Text("Screen 3")
.tabItem {
Image(systemName: "ellipsis")
Text("More")
}
}
}
The actual result looks like this:
Is it possible to position TabView below the content layout, not on top of it? Or the option would be to set bottom padding on content view (and in this case it would be required to check the TabView height).
The contentView layout looks like this:
VStack {
List() { ... }
HStack { Button() Spacer() Button() ... }
}
The contentView contains a List, and it fills all available space, moving buttons that are located below the List under the TabView. Adding Space after the contentView solves the issue.
var body: some View {
TabView {
VStack {
self.contentView
Spacer()
}
.tabItem {
Image(systemName: "chart.bar.fill")
Text("Chart")
}
Text("Screen 2")
.tabItem {
Image(systemName: "person.3.fill")
Text("Leaderboard")
}
Text("Screen 3")
.tabItem {
Image(systemName: "ellipsis")
Text("More")
}
}
}
Thank you #Asperi for pointing in right direction, and checking the children of the contentView.
Just add to your content view:
.padding(.bottom, tabBarHeight)

Pin text to bottom of scroll view SwiftUI

How can I pin a view element to the bottom of a scrollview/VStack in SwiftUI? I've tried using spacers but that doesn't seem to be working. I need the 'Footer' text to be at the bottom of the scroll view/Vstack, how can I do that?
struct ContentView: View {
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
Text("Top Title")
Text("Body")
Spacer()
Text("Footer") // SHOULD BE PINNED TO BOTTOM OF SCROLL VIEW
.frame(alignment: .bottom)
}
}
.background(Color.red)
Button("Done") {
//some action
}
}
}
}
Here is possible approach:
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
Text("Top Title")
Text("Body")
}
}
.overlay(
Text("Footer")
, alignment: .bottom) // << here !!
Guess 2: probably you wanted this
GeometryReader { gp in
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
Text("Top Title")
Text("Body")
Spacer()
Text("Footer") // SHOULD BE PINNED TO BOTTOM OF SCROLL VIEW
.frame(alignment: .bottom)
}
.frame(maxWidth: .infinity, minHeight: gp.size.height)
}
.background(Color.red)
}
You are properly looking for the safeAreaInset modifier: https://developer.apple.com/documentation/swiftui/menu/safeareainset(edge:alignment:spacing:content:)-1dqob/

How to make a List background black in SwiftUI?

What I did to make the entire screen black is to use this construction in my view:
ZStack {
Color.black.ignoresSafeArea() }
And for the List modifiers I used the following two:
.listRowBackground(Color.black)
.listStyle(.plain)
This works great.
But then I want to add a view on top of my List (like a header) and some white space appears around it ? Where does that come from ? And how can I change it to black ?
Here is my complete code:
struct WorkoutView: View {
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
List {
TempView()
ForEach(1..<30) { index in
VStack(alignment: .leading, spacing: 4) {
Text("one line")
.foregroundColor(.white)
.font(.title)
.fontWeight(.bold)
}
.padding(.vertical)
}
.listRowBackground(Color.black)
}
.listStyle(.plain)
}
}
}
struct TempView: View {
var body: some View {
ZStack(alignment: .leading) {
Color.black.ignoresSafeArea()
Text("Hello World")
.foregroundColor(.red)
.font(.largeTitle)
.bold()
}
}
}
Just do the same thing you did for the ForEach: add a .listBackground() modifier. Like this:
TempView()
.listRowBackground(Color.black)

SwiftUI content is not displayed on top of the screen

I have a NavigationView and then a VStack. The issue is that everything inside VStack is being displayed in the middle of the screen and not on top. Why is that?
var body: some View {
VStack {
VStack(alignment: .leading) {
if (request?.receiverID ?? "") == userDataViewModel.userID {
Text("\(sendertFirstName) wants to ship your package").font(.title)
} else {
Text("You want to ship \(recieverFirstName)'s package").font(.title)
}
HStack{
Image(systemName: "clock.fill").font(.subheadline)
Text("\(createdAt, formatter: DateService().shortTime)").font(.subheadline).foregroundColor(.secondary)
}
HStack {
VStack(alignment: .leading) {
HStack {
Image(systemName: "person.fill").font(.subheadline)
Text(sendertFirstName).font(.subheadline).foregroundColor(.secondary)
}
}
Spacer()
VStack(alignment: .leading) {
HStack {
Image(systemName: "star.fill")
Text(rating).font(.subheadline).foregroundColor(.secondary)
}
}
}
}
VStack(alignment: .center) {
HStack {
Button("Accept") {
//print(self.request.createdAt)
//FirestoreService().respondToRequest(request: self.request.documentReference, status: "accepted")
}
.padding()
Button("Decline") {
//FirestoreService().respondToRequest(request: self.request.documentReference, status: "declined")
}
.foregroundColor(.red)
.padding()
}
}
ScrollView {
ForEach(messages) { (message: Message) in
if message.senderId == self.userDataViewModel.userID {
HStack {
Spacer()
Text(message.text).font(.subheadline).padding(7.5).background(self.blue).cornerRadius(30).foregroundColor(.white)
}.padding(.bottom, 2)
} else {
HStack {
Text(message.text).font(.subheadline).padding(7.5).background(self.gray).cornerRadius(30).foregroundColor(.white)
Spacer()
}.padding(.bottom, 2)
}
}
}
HStack {
TextField("Message", text: $inputMessage).padding(5).background(self.gray).cornerRadius(30).foregroundColor(.white)
Button(action: {
let msg: [String: Any] = [
"text": self.inputMessage,
"createdAt": Timestamp(),
"senderId": self.userDataViewModel.userID
]
self.reference.updateData([
"messages": FieldValue.arrayUnion([msg])
])
self.messages.append(Message(dictionary: msg))
self.inputMessage = ""
}) {
Image(systemName: "arrow.up.circle.fill").foregroundColor(self.blue).font(.title)
}
}
Spacer()
}
.padding()
.border(Color.red)
}
I want the content to start on top not on the middle. What am I doing wrong here?
You don't need the NavigationView - the way you have everything set up now you are wrapping the VStack in a NavigationView twice and the massive white space is the second empty navigation bar.
By default most Views take only the amount of space they need and they are placed in the center of their parent view. So if you want your content to be placed at the top, you need to add Spacer() as the last element in your VStack:
var body: some View {
VStack {
/* ... */
Spacer()
}
}
Spacer is one of the rare Views that takes all space offered to it by the parent view and will push all of your content upwards.
Maybe you can try this way:
NavigationView {
VStack {
...
}.frame(maxHeight:.infinity, alignment: .top)
.padding()
.border(Color.red).navigationBarTitle("", displayMode: .inline)
}
Add a .navigationBarHidden(true) to your VStack

Center Text in a Button

I've got the following View:
HStack {
...
VStack {
...
Button(action: model.signIn) {
HStack {
Text("Sign In")
Spacer()
}
}.relativeHeight(2).background(Color.green).cornerRadius(5)
}
...
}
This allows me to create the following UI:
The Spacer inside the HStack and Button was a nice hack that made the button extend to the width of its parent. However, the text is still sitting at the leading position.
Anyone know of a way to centre the text inside the button?
You can do it in two ways,
Remove the Spacer after the text - this will shrink the button to
match txt size.
Add a Spacer before text. like
Code:
HStack {
...
VStack {
...
Button(action: model.signIn) {
HStack {
Spacer()
Text("Sign In")
Spacer()
}
}.relativeHeight(2).background(Color.green).cornerRadius(5)
}
...
}
HStack {
...
VStack {
...
Button("Sign In") {
model.signIn()
}
.frame(maxWidth: .infinity, alignment: .center).padding()
.background(Color.green).cornerRadius(5)
}
...
}