SwiftUI ScrollView only scroll in one direction - swift

Trying to make a custom list using a view as the list row style (to get rid of the ugly line separates in the list by default).
However, once I put my ZStack rows inside a scroll view, the scroll view scrolls in both directions and not just vertically.
Here is the contentView:
NavigationView {
ScrollView{
VStack(alignment: .leading){
ForEach(friends) { friends in
NavigationButton(destination: MessageDetailView(friend: friends)) {
CustomListItem(friend: friends)
}
}
Spacer()
}
}
.foregroundColor(.black)
.navigationBarTitle(Text("Messages"))
}
and here is the customListItem:
Group{
ZStack {
RoundedRectangle(cornerRadius: 10)
.shadow(radius: 1, y:1)
.frame(width: UIScreen.main.bounds.width - 32, height: 75)
.foregroundColor(.gray)
HStack {
Image(systemName: "person.crop.circle")
.resizable()
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
HStack {
Text(friend.name)
.font(.headline)
Text("\(friend.date, formatter: dateFormatter)")
}
Text(friend.messagesReceived[0])
.font(.subheadline)
} .lineLimit(nil)
Spacer()
Image(systemName: "chevron.right.circle")
.foregroundColor(.black)
}.padding(10)
}.padding([.leading, .trailing])
}
Is there any way I can limit the scrolling to vertical or force a frame on this?
Trying to use the .frame(...) modifier does not work as I've tried it. This results in the view not loading at all.
Example Images:

As of Xcode 11 beta 3 (11M362v) there is an axes parameter when constructing a ScrollView which can be set to .horizontal or .vertical
ScrollView(.vertical, showsIndicators: true) { ... }

This can be achieved with a GeometryReader. Wrap your ScrollView in one and then set the width of your VStack.
GeometryReader is a super easy, and pretty useful trick to have in your belt for SwiftUI :)
Here's how I got it working with your code:
NavigationView {
GeometryReader { geometry in
ScrollView {
VStack(alignment: .leading){
ForEach(self.friends) { friend in
NavigationButton(destination: MessageDetailView(friend: friend)) {
CustomListItem(friend: friend)
}
}
Spacer()
}.frame(width: geometry.size.width)
}
.foregroundColor(.black)
.navigationBarTitle(Text("Messages"))
}
}

Using iOS 14, Swift 5, SwiftUI 2.0
This is the answer... you can use both vertical and horizontal in a set.
ScrollView([.vertical,.horizontal]) {
....
}

Although in most cases the above answers works but none of them did work for me!
In my case, the problem was the scrollView children which were wider than the scrollView!
adding a static width to them did the trick.

Try being more specific with the scroll direction like this
ScrollView(.vertical) {}

Related

How to make a clear SwiftUI view block the scrollview underneath it

So I have a ZStack that contains a ScrollView on the bottom and an HStack that's aligned at the top. It looks something like this:
ZStack {
ScrollView {
//Content
}
VStack {
HStack {
Spacer()
Circle()
.frame(width: 30, height: 30)
Spacer()
}
Spacer()
}
}
Now, I want that HStack to block any interaction with the ScrollView beneath it. I've noticed that when I set the background on the HStack to a non-clear color, it behaves as I'd like it to. However, if the background is clear, then the touches go through the HStack and interact with the ScrollView beneath it. I've also tried using .allowsHitTesting(true) on the HStack with no luck.
Any help is much appreciated!
https://i.stack.imgur.com/5Jzby.png
"...I've noticed that when I set the background on the HStack to a non-clear color, it behaves as I'd like it to.", then you could use Color.white.opacity(0.001)
instead of Color.clear or non-clear color
You can easily accomplish this by putting it in a VStack instead of a ZStack. The touches will be ignored as it's above the ScrollView. You can also have a look at .layoutPriority(), it may also be useful. Documentation is here.
struct ContentView: View {
var body: some View {
VStack {
HStack {
Spacer()
Circle()
.foregroundColor(Color.blue)
.frame(width: 30, height: 30)
Spacer()
}
.padding()
// or this does the same thing as the HStack two spacers
/*
Circle()
.foregroundColor(Color.blue)
.frame(width: 30, height: 30)
.frame(minWidth: 0, maxWidth: .infinity)
*/
Spacer()
ScrollView {
ForEach(1..<11) { value in
Text("\(value)")
}
}
}
}
}

SwiftUI set blur on scrollview children except top most child

How would I set a blur on all of a scrollview's children (inside a vstack) except for the topmost visible child in the scrollview?
I've tried something like the following but it does not work.
ScrollViewReader { action in
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(lyrics, id: \.self) { lyric in
Text(lyric)
.onAppear {
blurOn = 0.0
}
.onDisappear {
blurOn = 5.0
}
.padding()
.blur(radius: blurOn)
}
}
.font(.system(size: 20, weight: .medium, design: .rounded))
.padding(.bottom)
}
.cornerRadius(10)
.frame(height: 130)
}
After thinking about it, I guess I could put the VStack in a ZStack and place a 0 opacity view on the bottom with a blur...just wondering if there's a better way to do this?

Spacer() swiftui leaves a white space

I'm currently building a UI that has a big red 250px height box on the top that holds information and a scrollview underneath it. I build this all in a VStack(). I put a spacer() at the bottom of the VStack to push everything to the top of the screen. But it has a big white space between the red container at the top and the scrollview. Even though both don't have any padding(). Does anyone know how this is possible? This is my code:
import SwiftUI
struct User: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
NavigationView {
VStack {
...
code
...
}
.frame(width: UIScreen.main.bounds.width, height: 250)
.background(Color.red)
.ignoresSafeArea()
// somehow it has a white space here...
ScrollView {
LazyVGrid(columns: columns, spacing: 2) {
ForEach((0...56), id: \.self) {_ in
Image("testimage")
.resizable()
.scaleEffect(CGSize(width: 1.0, height: 2.0))
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)
.cornerRadius(10)
}.padding(.bottom, 75)
}
.frame(width: UIScreen.main.bounds.width)
.padding(.top, -10)
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
// this Spacer should push everything to the top
Spacer()
}
}
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
Edit:
To make my question better understandable I made this summery:
I have build a VStack with 2 items in it. there is a big white space between the two and I don't know how to get rid of this white space. If I remove .ignoresSafeArea() the white space disappears. The thing is I need .ignoresSafeArea() to push the first item in VStack up. Does anyone know what I can do to keep .ignoresSafeArea() but get rid of the white space?
I added a screenshot of the problem. I gave the scrollview a green background so that the white spacing between the VStack and the Scrollview is visable.
You need to embed everything in a VStack with 0 spacing.
import SwiftUI
struct User: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
NavigationView {
VStack(spacing: 0) {
VStack {
Text("Hello World!") //Easier to test
}
.frame(height: 250) //Dont use UIScreen
.background(Color.red)
.ignoresSafeArea()
// somehow it has a white space here...
ScrollView {
LazyVGrid(columns: columns, spacing: 2) {
ForEach((0...56), id: \.self) {_ in
Image("testimage")
.resizable()
.scaleEffect(CGSize(width: 1.0, height: 2.0))
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)
.cornerRadius(10)
}.padding(.bottom, 75)
}
.frame(width: UIScreen.main.bounds.width)
.padding(.top, -10)
}
Spacer()
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
}
Also, you might need to give the ScrollView a max height to push it upwards. .frame(maxHeight: 175)
As you haven't given a reproducible example or an image. I cant tell you for certain what will work
I guess Arnavs Answer is correct.
Notice:
When you are trying to implement multiple Views on one page, 90% of the time it is better to have them in one H/V/Z-Stack.
For a better understanding start a new project and try ...
Navigationview{
VStack{
Text("Hello Vertical World")
Text("Hello Vertical World2")
}
HStack{
Text("Hello Horizontal World")
Text("Hello Horizontal World2")
}
}
and the same thing but "different"
NavigationView {
VStack{
Text("Hello Vertical World")
Text("Hello Vertical World2")
HStack{
Text("Hello Horizontal World")
Text("Hello Horizontal World2")
}
}
}
I can't double check bc i am on my phone, but this should look very different.
I fixed the issue by putting the VStack and the Scrollview in an other VStack in the navigationView. Then I put the .ignoresSafeArea() on this new VStack.
Code:
import SwiftUI
struct User: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
NavigationView {
// new VStack that includes the topbar VStack, the
// scrollview and has the .ignoresSafeArea() on it.
VStack {
VStack {
...
Code topbar
...
}
.frame(width: UIScreen.main.bounds.width, height: 250)
.background(Color.red)
Scrollview {
...
Code scrollview
...
}
Spacer()
}
.ignoresSafeArea()
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}

How to move this to the top?

I just learned how to implement specific rounded corners, but now it seems nothing will align to the top of the screen, even with spacers. How can I get it to align to the top of the screen?
Additionally, I would like the green to ignore the top safe area, but it wasn't doing that earlier either.
import SwiftUI
struct Dashboard: View {
#State var bottomLeft: CGFloat = 25
var body: some View {
ZStack {
Color("background")
.edgesIgnoringSafeArea(.all)
VStack {
VStack {
Text("Good Morning, Sarah")
.font(Font.system(size: 36))
.foregroundColor(.standaloneLabelColor)
.fontWeight(.semibold)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
Text("We're glad to see you and hope you're doing well. Let's take on the day.")
.font(Font.system(size: 20))
.foregroundColor(.standaloneLabelColor)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.padding(.bottom)
}
.background(Color.appColor)
.cornerRadius(bottomLeft, corners: .bottomLeft)
.padding(.bottom, 10)
}
Spacer()
}
}
}
Thanks in advance.
You can set the alignment on the ZStack to .top. You can then also remove the Spacer.
A Spacer does nothing in a ZStack - since it's on its own layer. But setting the alignment on the ZStack will align all views to the top. If this is not what you want, you can also just put the Spacer in the inner VStack and the contents of that will all be aligned to the top also.
Code:
ZStack(alignment: .top) {
Color("background")
.edgesIgnoringSafeArea(.all)
VStack {
/* ... */
}
}
Also, to ignore the safe area at the top when applying the rounded corners, you should clip the corners and use ignoresSafeArea() within background. This ensures that you are only ignoring the safe area for the background, and not the whole view. Do the following:
.background(
Color.appColor
.cornerRadius(bottomLeft, corners: .bottomLeft)
.ignoresSafeArea()
)
// .background(Color.appColor)
// .cornerRadius(bottomLeft, corners: .bottomLeft)
.padding(.bottom, 10)
I had to remove some bits from your code as they were producing errors on my end. Working with what I had, you needed to add a Spacer() below the appropriate VStack.
Since your content was stored in a VStack embedded in another VStack, the outer VStack was essentially where the entire view lived. Putting a Spacer beneath this pushes it up to the top of the screen.
You can additionally add padding to the top of the VStack to move the view lower if you do not want it touching the top of the screen.
Code below:
import SwiftUI
struct ContentView: View {
#State var bottomLeft: CGFloat = 25
var body: some View {
ZStack {
Color("background")
.edgesIgnoringSafeArea(.all)
VStack {
VStack {
Text("Good Morning, Sarah")
.font(Font.system(size: 36))
.foregroundColor(Color.blue)
.fontWeight(.semibold)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
Text("We're glad to see you and hope you're doing well. Let's take on the day.")
.font(Font.system(size: 20))
.foregroundColor(Color.blue)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.padding(.bottom)
}
.background(Color.green)
.cornerRadius(bottomLeft)
.padding(.bottom, 10)
Spacer() //Added a spacer here
}
}
}
}

How can I fit a shape in swift ui to accommodate the length and width of a text view

ZStack {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(.red)
.scaledToFit() //.frame(width: 200, height: 25)
HStack {
Image(systemName: "tag.fill")
.foregroundColor(.white)
Text("Tickets Not Available")
.font(.headline)
.foregroundColor(.white)
.fixedSize(horizontal: true, vertical: false)
}
}
.scaledToFit()
As you can see my views are placed in a zstack so that the rounded rectangle can be the background of the text view. I've tried so many different things like where to put the .scaledtofit and it just gives me wack results each time.
is this what you're after (note the Image.resizable):
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack{
RoundedRectangle(cornerRadius: 8).foregroundColor(.blue)
HStack{
Image(systemName: "tag.fill").resizable().padding(4).foregroundColor(.white).scaledToFit()
Text("Get Tickets").font(.headline).foregroundColor(.white)
}
}.fixedSize()
}
The question is a bit unclear but if you are trying to fit a shape inside the text view, and you are fine with getting rid of scaledToFit, then the code should be:
RoundedRectangle(cornerRadius: 8).foregroundColor(.red).frame(width: textView.width, height: textView.height)
Hope this helps, and hopefully you didn't need to use scaledToFit.
If you did tell me in comments.
A reusable ButtonStyle might be helpful here. Instead of a ZStack, using the .background modifier helps to keep the size of the Button contents:
struct RoundedButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
configuration.label
.font(.headline)
.padding()
.background(RoundedRectangle(cornerRadius: 8).foregroundColor(Color.blue))
}
}
}
Usage example:
Button(
action: {
print("Button Tapped")
},
label: {
HStack {
Image(systemName: "tag.fill")
Text("Tickets")
}
}
)
.buttonStyle(RoundedButtonStyle())