Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pager's scroll blocks the ScrollView's scroll #18

Closed
NeverwinterMoon opened this issue Feb 27, 2020 · 16 comments · Fixed by #37
Closed

Pager's scroll blocks the ScrollView's scroll #18

NeverwinterMoon opened this issue Feb 27, 2020 · 16 comments · Fixed by #37
Assignees
Labels
bug Something isn't working

Comments

@NeverwinterMoon
Copy link

I've seen there was a similar issue (#3) but then it was closed and an entirely different use case with ScrollView was discussed, so I am going to post this.

Basically, if a Pager view is placed inside a ScrollView with vertical scroll, the former blocks the parent's scroll, unless the drag gesture starts outside the Pager's view area.

This is not an issue caused by Pager itself, but there might be solutions that would work if applied to the library. I haven't seen any perfect solutions yet though.

There are some discussions here: https://stackoverflow.com/questions/57700396/adding-a-drag-gesture-in-swiftui-to-a-view-inside-a-scrollview-blocks-the-scroll. The trick with the button (https://stackoverflow.com/a/59961959/1719285) can be applied outside the library, but this then blocks the tap gestures inside each view (in my case, the remove button). The only solution that worked more or less OK (ish) was to have DragGesture(minimumDistance: 30, coordinateSpace: .local) for the swipeGesture of the library.

@NeverwinterMoon NeverwinterMoon changed the title SwiftUIPager: Pager's scroll blocks the ScrollView's scroll Pager's scroll blocks the ScrollView's scroll Feb 27, 2020
@fermoya
Copy link
Owner

fermoya commented Feb 27, 2020

HI @NeverwinterMoon ,

Thanks again for your feedback. I remember this issue, I finally chose to use highPriorityGesture to make sure Pager would scroll as I was having trouble when Pager contained a ScrollView or was presented modally.

When contained in a ScrollView is new you're right to say it won't work as it is right now due to the high priority view modifier. I'll revisit this again although I seem to remember I tried using simultaneousGesture.

Gestures are something that I've been struggling a lot and I believe is something that needs a lot of improvement to resemble UIKit.

Thanks again, will be in touch soon.

@fermoya
Copy link
Owner

fermoya commented Mar 2, 2020

Hi @NeverwinterMoon ,

I've given a look at this and doesn't seem to work. You suggestion DragGesture(minimumDistance: 30, coordinateSpace: .local) works fine if the view is not presented but when it is, it breaks the pagination. Try this out:

    var body: some View {
        Button(action: {
            self.isPresented.toggle()
        }, label: {
            Text("Present")
        }).sheet(isPresented: $isPresented) {
            self.presented
        }
    }

    @State var data = Array(0..<10)

    var presented: some View {
        NavigationView {
            GeometryReader { proxy in
                ScrollView {
                    VStack {
                        HStack {
                        Pager(page: self.$page,
                              data: self.data,
                              id: \.self) {
                                self.pageView($0)
                        }.onPageChanged { page in
                            print(page)
                        }
                        .rotation3D()
                        .itemSpacing(10)
                        .itemAspectRatio(0.8, alignment: .end)
                        .padding(8)
                        .frame(width: min(proxy.size.height, proxy.size.width),
                               height: min(proxy.size.height, proxy.size.width))
                            .background(Color.gray.opacity(0.2))
                        }

                        Button(action: {
                            DispatchQueue.main.async {
                                self.data.remove(at: self.page)
                                self.page = min(self.page, self.data.count - 1)
                                print("\t\(self.data.count), \(self.page)")
                            }
                        }, label: {
                            Text("remove")
                        })

                        ForEach(Array(1...100), id: \.self) { _ in
                            Text("Item")
                        }
                    }
                }
            }.navigationBarTitle("SwiftUIPager", displayMode: .inline)
        }
    }

    func pageView(_ page: Int) -> some View {
        ZStack {
            Rectangle()
                .fill(Color.yellow)
            Text("Page: \(page)")
                .bold()
        }
        .cornerRadius(5)
        .shadow(radius: 5)
    }

This is the worst case I can think of. As it is right now, Pager takes the touch from ScrollView but this seems a lesser issue.

Fernando

@NeverwinterMoon
Copy link
Author

NeverwinterMoon commented Mar 2, 2020

The DragGesture(minimumDistance: 30, coordinateSpace: .local) workaround certainly doesn't break pagination in my tests, unless I am missing something...

Image from Gyazo

@fermoya
Copy link
Owner

fermoya commented Mar 2, 2020

Hi @NeverwinterMoon ,

Have you tried copy-pasting my code? Use a sheet to present a modal. Or download the code from branch pager-swiping and go to the "Presented" tab and use your code... You can also find it here

Fernando

@NeverwinterMoon
Copy link
Author

NeverwinterMoon commented Mar 2, 2020

@fermoya Aha, I tried it out now with PresentedExampleView. It does break pagination indeed. Sorry for the confusion. I do agree that Pager not being scrollable is a bigger issue that ScrollView not receiving touch events, but still, the latter is extremely annoying. Even in the PresentedExampleView example where the Pager takes most of the horizontal view, the user has an inconvenience of scrolling only at a very narrow view at the bottom.

Also, imagine a situation with multiple Pagers, something like in the App Store:

ScrollView
  Pager
  Pager
  Pager

The page will not be vertically scrollable at all, and this is certainly not usable.

@fermoya
Copy link
Owner

fermoya commented Mar 2, 2020

I totally agree, it's very annoying. When you have a ScrollView inside another scrollview it works fine:

ScrollView
    ScrollView(.horizontal)
    ScrollView(.horizontal)
    ScrollView(.horizontal)

even if it is presented... By specifying the axis, it is able to just react to horizontal swipes, which would be ideal for Pager but DragGesture doesn't seem to allow it. I can't manage to find a way to create a custom gesture myself by implementing Gesture.

Let's keep this open for future SwiftUI updates

@codetheweb
Copy link

codetheweb commented May 21, 2020

I'm having a very similar problem, I have a container that needs to react to vertical drags, and the pager should react to horizontal drags (basically, a clone of the Photos.app gallery view: swipe down to dismiss, swipe horizontally to page through).

I'm very new to Swift and SwiftUI, but I was able to make it work by only updating the offset if the translation was within a certain sector (i.e. the angle was within the given bounds).

This is the gesture handler for my container:

.simultaneousGesture(DragGesture()
        .updating($offset, body: { (value, state, transaction) in
            let translation = value.translation
            let angle = CGPointToDegree(value.startLocation - value.location)
            
            print(angle)

            let isInBounds = (20 < angle && angle < 160) || (-160 < angle && angle < -20)

            if (isInBounds) {
                state = translation
            }
            
        })

the edited .onChange handler in Pager.swift:

.onChanged({ value in
                let angle = CGPointToDegree(value.startLocation - value.location)

                let isInBounds = (160 < angle && angle < 180) || (-180 < angle && angle < -160) || (-20 < angle && angle < 20)

                if (isInBounds) {
                    withAnimation {
                        self.draggingStartTime = self.draggingStartTime ?? value.time
                        self.draggingOffset = value.translation.width
                    }
                }
            })

and some hacked-together helpers:

func CGPointToDegree(_ point: CGPoint) -> Double {
  // Provides a directional bearing from (0,0) to the given point.
  // standard cartesian plain coords: X goes up, Y goes right
  // result returns degrees, -180 to 180 ish: 0 degrees = up, -90 = left, 90 = right
  let bearingRadians = atan2f(Float(point.y), Float(point.x))
    let bearingDegrees = Double(bearingRadians) * (180 / .pi)
  return bearingDegrees
}

func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
    return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}

This is specific to my application, but I also ended up changing item spacing when the user is dragging the Pager so that it doesn't get offsetted enough that normally hidden items come onto the display:

.itemSpacing(self.offset == .zero ? 30 : 10000)

The combination of angle checks and item spacing seems to work fairly well.

Hope that helps.

@fermoya
Copy link
Owner

fermoya commented May 22, 2020

Hi @codetheweb,

Thanks for your comment. There's an sample App in the repository which has a tab called "Presented":
Screenshot 2020-05-22 at 08 33 57
This is an edge situation. Now, your code is skipping scrolling the pages if the dragging isn't in the horizontal axis. But the gesture is already recognized! This means it won't let the ScrollView scroll or the presented view to be dismissed.

In UIKit, if you create a custom gesture which should only be triggered when scrolling horizontally, you make it fail when scrolling vertically. This way the gesture isn't recognized and some other View can try to recognize it. This isn't the case with SwiftUI, there's not such thing.

The best workaround is suggested above, but again it introduces an issue with the "Presented" tab in the Sample project

fermoya pushed a commit that referenced this issue May 22, 2020
Adding minimum distance to DragGesture and modifier to set gesture as high priority
fermoya pushed a commit that referenced this issue May 22, 2020
@fermoya fermoya self-assigned this May 22, 2020
@fermoya
Copy link
Owner

fermoya commented May 22, 2020

@NeverwinterMoon I'm gonna add that workaround you suggested and at the same time a modifier highPriorityGesture to reset to as it is currently

@fermoya fermoya linked a pull request May 22, 2020 that will close this issue
@fermoya fermoya added the bug Something isn't working label May 24, 2020
@fermoya
Copy link
Owner

fermoya commented May 24, 2020

Version 1.5.0 released. Using suggested hack till SwiftUI offers new options. Pager works fine inside a ScrollView now.

In case Pager is needed in a interactive modal, please use highPriorityGesture.

@fermoya fermoya closed this as completed May 24, 2020
@codetheweb
Copy link

@fermoya I just tried this and using highPriorityGesture didn't seem to work. My custom gesture attached to the Pager triggered, but I was not able to swipe through the Pager. I'm probably just doing something wrong or not understanding SwiftUI, but would simultaneousGesture work better here instead?

@fermoya
Copy link
Owner

fermoya commented May 25, 2020

Hi @codetheweb ,

highPriorityGesture is meant when embedded in an interactive modal. Just embed Pager inside a ScrollView and should work fine. See EmbeddedExampleView in the Sample Project.

@codetheweb
Copy link

Sorry, I'm unclear on what you're saying.

I should embed Pager inside a vertical ScrollView, then attach any gestures to that ScrollView?

This modified example doesn't really work:

//
//  EmbeddedExampleView.swift
//  SwiftUIPagerExample
//
//  Created by Fernando Moya de Rivas on 02/03/2020.
//  Copyright © 2020 Fernando Moya de Rivas. All rights reserved.
//

import SwiftUI

struct EmbeddedExampleView: View {
    @State var page: Int = 0
    var data = Array(0..<10)
    @State var draggingOffset: CGSize = .zero

    var colors: [Color] = [
        .red, .blue, .black, .gray, .purple, .green, .orange, .pink, .yellow, .white
    ]

    var body: some View {
        NavigationView {
            GeometryReader { proxy in
                ScrollView {
                    VStack {
                        Pager(page: self.$page,
                              data: self.data,
                              id: \.self) { page in
                                self.pageView(page)
                        }
                        .rotation3D()
                        .itemSpacing(10)
                        .itemAspectRatio(0.8, alignment: .end)
                        .padding(8)
                        .frame(width: min(proxy.size.height, proxy.size.width),
                               height: min(proxy.size.height, proxy.size.width))
                            .background(Color.gray.opacity(0.2))
                    }
                }
                .offset(self.draggingOffset)
                .gesture(DragGesture().onChanged { value in
                    withAnimation {
                        self.draggingOffset = value.translation
                    }
                })
            }.navigationBarTitle("SwiftUIPager", displayMode: .inline)
        }
    }

    func pageView(_ page: Int) -> some View {
        ZStack {
            Rectangle()
                .fill(Color.yellow)
            Text("Page: \(page)")
                .bold()
        }
        .cornerRadius(5)
        .shadow(radius: 5)
    }

}

If further clarification is needed I can open a new issue.
(And thanks for the support, I really appreciate it.)

@fermoya
Copy link
Owner

fermoya commented May 27, 2020

Hi @codetheweb ,

I don't understand the example. Why do you have a DragGesture applied to a ScrollView to change its offset? What are you trying to achieve exactly?

Maybe you're right and you should open a new issue and we discuss it there.

@codetheweb
Copy link

I thought your comment meant that I should embed Pager inside ScrollView so that it only reacted to horizontal gestures.

But I was able to get it working by using the allowsDragging() modifier that you recently added along with simultaneousGesture() (attached directly to the Pager instance). I just set allowsDragging to false when the simultaneousGesture is activated, seems to work well.

By the way, you should set up GitHub Sponsors, Buy me a coffee, or similar. I'd be happy to send something your way and I'm sure others would too. 😄

@fermoya
Copy link
Owner

fermoya commented May 28, 2020

thanks for the suggestion, @codetheweb . I was thinking of that, might do in the future 😄

Thanks a lot and apologies for the confusion

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants