Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interaction of DragGesture and ScrollView in SwiftUI

In an app I'm working on, there is a part that has, mostly, a "forward" navigation – tapping on buttons would display the next slide. However, a secondary "backward" navigation is also necessary. Here's the approach I've used:

import SwiftUI

struct Sample: View {
    @State private var dragOffset: CGFloat = -100
    var body: some View {
        VStack {

            Text("Perhaps a title")

            ScrollView {
                VStack {
                    Text("Some scrollable content is going to be here")

                    // ...

                    Button(action: {
                        // Go to the next slide
                    }) { Text("Next") }
                }
            }

            Text("and, maybe, something else")
        }
        .overlay(
            Image(systemName: "arrow.left").offset(x: dragOffset / 2),
            alignment: .leading
        )
        .gesture(
            DragGesture()
                .onChanged{
                    self.dragOffset = $0.translation.width
                }
                .onEnded {
                    self.dragOffset = -100 // Hide the arrow

                    if $0.translation.width > 100 {
                        // Go to the previous slide
                    }
                }
        )
    }
}

There is a small indicator (left arrow) that is, initially, hidden (dragOffset = -100). When the drag gesture begins, offset is fed into the dragOffset state variable and that, effectively, shows the arrow. When drag gesture ends, the arrow is hidden again and, if a certain offset is reached, the previous slide is displayed.

Works well enough, except, when the user scrolls the content in the ScrollView, this gesture is also triggered and updated for a while but then is, I assume, cancelled by the ScrollView and the "onEnded" is not called. As a result, the arrow indicator stays on the screen.

Hence the question: what is the correct way to do a gesture like that, that would work together with a ScrollView? Is it even possible with the current state of SwiftUI?

like image 816
Baglan Avatar asked Feb 19 '20 16:02

Baglan


1 Answers

For such temporary states it is better to use GestureState as it is automatically reset to initial state after gesture cancels/finished.

So here is possible approach

Update: retested with Xcode 13.4 / iOS 15.5

Demo:

enter image description here

Code:

struct Sample: View {
    @GestureState private var dragOffset: CGFloat = -100
    var body: some View {
        VStack {

            Text("Perhaps a title")

            ScrollView {
                VStack {
                    Text("Some scrollable content is going to be here")

                    // ...

                    Button(action: {
                        // Go to the next slide
                    }) { Text("Next") }
                }
            }

            Text("and, maybe, something else")
        }
        .overlay(
            Image(systemName: "arrow.left").offset(x: dragOffset / 2),
            alignment: .leading
        )
        .gesture(
            DragGesture()
                .updating($dragOffset) { (value, gestureState, transaction) in
                    let delta = value.location.x - value.startLocation.x
                    if delta > 10 { // << some appropriate horizontal threshold here
                        gestureState = delta
                    }
                }
                .onEnded {
                    if $0.translation.width > 100 {
                        // Go to the previous slide
                    }
                }
        )
    }
}

Note: dragOffset: CGFloat = -100 this might have different effect on different real devices, so probably it is better to calculate it explicitly.

backup

like image 64
Asperi Avatar answered Oct 31 '22 04:10

Asperi