Vertical ScrollView disabled when cursor is inside horizontal ScrollView in SwiftUI - swift

My SwiftUI app has a List of ScrollView(.horizontal). I would like to vertical scroll to go through each of the horizontal scroll views (Rows) and to horizontal scroll within the Row. The following code accomplishes this :
struct Row: View {
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(0..<100) { i in
Text(String(i))
.padding(5)
}
}
.padding(10)
.background(.red)
}
}
}
struct ContentView: View {
var body: some View {
List(0..<100) {_ in
Row()
}
.background(.blue)
}
}
The issue is, if my cursor is inside the bounds of a Row, the outer vertical scroll is disabled. How can I "promote" the vertical scroll gesture of the nested Row to the List it lives in?
The reason I want this behavior is so that if the user is scrolling through the List, I don't want them to reposition their cursor if it lands inside the bounds of a Row.
Vertical Scroll: GIF
Horizontal Scroll: GIF
Issue

If you exchange the List with another vertical ScrollView it works both ways (don't ask me why).
struct ContentView: View {
var body: some View {
ScrollView(.vertical) {
ForEach(0..<100) { row in
Row()
}
}
.background(.blue)
}
}

Related

Moving view with DragGesture while having a ScrollView on top

I have a view that can be dragged by the user. This view contains a ScrollView that shows a list of information. However, this view interferes with the DragGesture of the containing view.
The expected behavior would be that only when the user can actually scroll something, the gesture of the ScrollView would have priority over the container.
Here is the code that can be used to test the problem I'm referring to:
struct MultipleDrag: View {
#GestureState private var offset: CGFloat = 0
private var gesture: some Gesture {
DragGesture()
.updating($offset) { value, state, transaction in
state = -value.translation.height
}
}
var body: some View {
VStack {
ScrollView {
ForEach(1...20, id: \.self) { val in
Text(String(val))
.padding()
}
}
.background(Color.green)
.cornerRadius(10)
.padding()
}
.frame(height: 200)
.background(Color.orange)
.cornerRadius(10)
.offset(y: -offset)
.gesture(gesture)
}
}
As you can see, by default the scrollview starts at the top. That would mean that if the user swiped down, the orange container should move downwards, even if the gesture took place in the green area (ScrollView). The same should happen when the bottom of the ScrollView was reached or when there's not enough content on the scrollview to actually scroll.
Any ideas?
I think you can use this modifier:
.highPriorityGesture(Gesture)
This will make the gesture with a higher priority than other gestures.

How to disable vertical bounce in SwiftUI on a single ScrollView

I have a half modal view coming from the bottom of the screen with a scrollView, and when there's isn't enough content to scroll I want the drag gesture on the internal scrollView to apply to the modal and expand it or collapse it.
I tried using:
init() {
UIScrollView.appearance().bounces = false
}
And it works fine but this disables the bouncing effect on all the scrollViews in my app.
Is there a way to apply this for a single ScrollView or at least a single View?
you can add this to a ViewModifier:
struct SomeModifier: ViewModifier {
init() {
UIScrollView.appearance().bounces = false
}
func body(content: Content) -> some View {
return content
}
}
ScrollView {
Text("Some scroll view")
}.modifier(SomeModifier()

SwiftUI - ScrollView has 0 width and my content is not visible

I am trying to use a scroll view for scrollable content, but whenever I nest my views inside the Scroll View, I have noticed that the views from my stacks vanish back into the view hierarchy and nothing remains visible on the screen. I have also seen that whenever I am using a ScrollView, it adds another Hosting View Controller and I don't know if this is the normal behaviour.
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ForEach(bookLibrary.indices, id: \.self) { index in
HStack {
ForEach(self.bookLibrary[index], id: \.self) { book in
BookView(book: book)
}
}
}
}
}
}
}
Getting this view hierarchy. You can also see that the HostingScrollView has a width of 0.
If you don't want to use GeometryReader just insert zero height view with correct width like this
var body: some View {
ScrollView {
VStack {
Color.clear
.frame(maxWidth: .infinity, maxHeight: 0)
ForEach(...) { each in
...
}
}
}
}
While not a perfect solution, you can use GeometryReader to set the scroll view's frame to the same width as its superview.
NavigationView {
GeometryReader {geometry in
ScrollView(.vertical) {
// TODO: Add content
}
.frame(width: geometry.size.width)
}
}
This workaround was inspired by Rob Mayoff's answer on another question.

`withAnimation` only does animation once when adding first item to #State array

My goal is to have control over the type of animation when an object is added to the #State events array.
withAnimation only occurs on the first append to the events array. It is then ignored on additional appends.
I'm currently running this on Xcode 11 beta 4
I've tried adding the calling DispatchQueue.main.async, having the animation on the Text() object.
If I use a list it performs animation on addition, however I don't know how to modify those animations.
Goal
Have text slide in with each append and fade out on each remove.
struct Event: Identifiable {
var id = UUID()
var title: String
}
struct ContentView: View {
#State
var events = [Event]()
var body: some View {
VStack {
ScrollView {
ForEach(events) { event in
Text(event.title)
.animation(.linear(duration: 2))
}
}
HStack {
Button(action: {
withAnimation(.easeOut(duration: 1.5)) {
self.events.append(Event(title: "Animate Please"))
}
}) {
Image(systemName: "plus.circle.fill").resizable().frame(width: 40, height: 40, alignment: .center)
}
}
}
}
}
I'm expecting that each append has an animation that is described in the withAnimation block.
When SwiftUI layouts and animations behave in ways you think are not correct, I suggest you add borders. The outcome may surprise you and point you directly into the cause. In most cases, you'll see that SwiftUI was actually right! As in your case:
Start by adding borders:
ScrollView {
ForEach(events) { event in
Text(event.title)
.border(Color.red)
.animation(.linear(duration: 2))
}.border(Color.blue)
}.border(Color.green)
When you run your app, you'll see that before adding your first array element, the ScrollView is collapsed into zero width. That is correct, as the ScrollView is empty. However, when you add your first element, it needs to be expanded to accommodate the "Animate Please" text. The Text() view also starts with zero width, but as its containing ScrollView grows, it does too. These are the changes that get animated.
Now, when you add your second element, there is nothing to animate. The Text() view is placed with its final size right from the start.
If instead of "Animate Please", you change your code to use a random length text, you will see that when adding a largest view, animations do occur. This is because ScrollView needs to expand again:
self.events.append(Event(title: String(repeating: "A", count: Int.random(in: 0..<20))))
What next: You have not explained in your question what animation you expect to see. Is it a fade-in? A slide? Note that in addition to animations, you may define transitions, which determines the type of animation to perform when a view is added or removed from your hierarchy.
If after putting these tips into practice, you continue to struggle, I suggest you edit your question and tell us exactly what animation would you like to see when adding a new element to your array.
UPDATE
According to your comments, you want the text to slide. The simplest form, is using a transition. Unfortunately, the ScrollView seems to disable transitions on its children. I don't know if that is intended or a bug. Anyway, here I post two methods. One with transitions (does not work with ScrollView) and one using only animations, which does work inside a ScrollView, but requires more code:
With transitions (does not work inside a ScrollView)
struct ContentView: View {
#State private var events = [Event]()
var body: some View {
VStack {
ForEach(events) { event in
// A simple slide
Text(event.title).transition(.slide).animation(.linear(duration: 2))
// To specify slide direction
Text(event.title).transition(.move(edge: .trailing)).animation(.linear(duration: 2))
// Slide combined with fade-in
Text(event.title).transition(AnyTransition.slide.combined(with: .opacity)).animation(.linear(duration: 2))
}
Spacer()
HStack {
Button(action: {
self.events.append(Event(title: "Animate Please"))
}) {
Image(systemName: "plus.circle.fill").resizable().frame(width: 40, height: 40, alignment: .center)
}
}
}
}
}
Without transitions (works inside a ScrollView):
struct Event: Identifiable {
var id = UUID()
var title: String
var added: Bool = false
}
struct ContentView: View {
#State var events = [Event]()
var body: some View {
VStack {
ScrollView {
ForEach(0..<events.count) { i in
// A simple slide
Text(self.events[i].title).animation(.linear(duration: 2))
.offset(x: self.events[i].added ? 0 : 100).opacity(self.events[i].added ? 1 : 0)
.onAppear {
self.events[i].added = true
}
}
HStack { Spacer() } // This forces the ScrollView to expand horizontally from the start.
}.border(Color.green)
HStack {
Button(action: {
self.events.append(Event(title: "Animate Please"))
}) {
Image(systemName: "plus.circle.fill").resizable().frame(width: 40, height: 40, alignment: .center)
}
}
}
}
}

Make ScrollView content fill its parent in SwiftUI

I'd love to build a scrolling screen, so I wanted to embed it ScrollView. But I am not able to achieve it the view just shrinks to its compressed size. Let's say that I want the ScrollView to scroll vertically so I'd love the content to match scrollView's width. So I use such preview:
struct ScrollSubview_Previews : PreviewProvider {
static var previews: some View {
func textfield() -> some View {
TextField(.constant("Text")).background(Color.red)
}
return Group {
textfield()
ScrollView {
textfield()
}
}
}
}
But it ends with result like this:
A simple way for you, using frame(maxWidth: .infinity)
ScrollView(.vertical) {
VStack {
ForEach(0..<100) {
Text("Item \($0)")
}
}
.frame(maxWidth: .infinity)
}
Here is a trick: introduce a HStack that only has one Spacer:
return Group {
HStack {
Spacer()
}
textfield()
ScrollView {
textfield()
}
}
Actually you don't need GeometryReader anymore. ScrollView has been refactored in Xcode beta 3. Now you can declare that you have a .horizontal or .vertical ScrollView.
This makes the ScrollView behave like it should, like any normal View protocol.
Ex:
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach((1...10).reversed()) {
AnyViewYouWant(number: $0)
}
}
}
This will result in a view with the width of its parent scrolling horizontally. The height will be defined by the ScrollView's subviews height.
It's a known issue existing also in beta 2 - check the Apple's Xcode 11 release notes here ⬇️
https://developer.apple.com/documentation/xcode_release_notes/xcode_11_beta_2_release_notes.
The workaround mentioned by Apple themselves is to set fixed frame for ScrollView inside element. In that case I suggest to use GeometryReader for fix screen width, height is automatically fit to content.
For example if you need to fit the ScrollView's content to screen width you can something like:
return GeometryReader { geometry in
Group {
self.textfield()
ScrollView {
self.textfield()
.frame(width: geometry.size.width)
}
}
}
You can also done this using extra HStack and space.
ScrollView(.vertical) {
HStack {
VStack {
ForEach(0..<100) {
Text("Item \($0)")
}
}
Spacer()
}
}
Scroll view content will expand to available size.
GeometryReader { geo in
ScrollView(.vertical) {
YourView().frame(
minWidth: geo.size.width,
minHeight: geo.size.height
)
}
}
PreviewProvider is just about your Canvas (rendering view).
if you want to place text into ScrollView you should create your View
struct MyViewWithScroll : View {
var body: some View {
ScrollView {
Text("Placeholder")
}
}
}
And after that, render your MyViewWithScroll in Canvas (window with iPhone from the right side of code editor)
#if DEBUG
struct MyViewWithScroll_Previews : PreviewProvider {
static var previews: some View {
MyViewWithScroll()
}
}
#endif