UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

A guide to delaying gestures in ScrollView

Forums > SwiftUI

Motivation

A gesture inside a scrolling view will hijack scrolling if the gesture is configured to be recognised first:

ScrollView {
    FooView()
        .gesture(DragGesture(minimumDistance: 0))
}

Preventing scrolling in certain areas would result in a poor user experience. In a real app this could be a custom interactive chart in a ScrollView. See an example here.

It's not always feasible to configure the gesture differently. In this example we need the location of the user's initial touch, so increasing the minimumDistance is not an option. In UIKit we could handle this by setting delaysContentTouches on the UIScrollView; SwiftUI does not have a similar option.

Solution 1

Many system controls such as Button, Menu and Picker have magic behind the scenes to delay touches in scrolling views. We can use onTapGesture to apply this magic to custom views:

ScrollView {
    FooView()
        .onTapGesture {}
        .gesture(DragGesture(minimumDistance: 0))
}

Unfortunately this doesn't provide control over how long the gesture is delayed for. With this solution the user has to hold down for 1 second before the gesture is recognised, which is too long for the example above.

Solution 2

As mentioned, Button also provides the same magic behaviour. We can create a ViewModifier that uses Button and a custom ButtonStyle to control how long the gesture is delayed for. In use:

ScrollView {
    FooView()
        .delaysTouches(for: 0.2) {
            toggleHelpPrompt()
        }
        .gesture(DragGesture(minimumDistance: 0))
}

We can change the duration to any non-negative TimeInterval to control the delay. We can also provide a function to be called when the user taps. Neither of these parameters are required. The implementation:

extension View {
    func delaysTouches(for duration: TimeInterval = 0.25, onTap action: @escaping () -> Void = {}) -> some View {
        modifier(DelaysTouches(duration: duration, action: action))
    }
}

fileprivate struct DelaysTouches: ViewModifier {
    @State private var disabled = false
    @State private var touchDownDate: Date? = nil

    var duration: TimeInterval
    var action: () -> Void

    func body(content: Content) -> some View {
        Button(action: action) {
            content
        }
        .buttonStyle(DelaysTouchesButtonStyle(disabled: $disabled, duration: duration, touchDownDate: $touchDownDate))
        .disabled(disabled)
    }
}

fileprivate struct DelaysTouchesButtonStyle: ButtonStyle {
    @Binding var disabled: Bool
    var duration: TimeInterval
    @Binding var touchDownDate: Date?

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .onChange(of: configuration.isPressed, perform: handleIsPressed)
    }

    private func handleIsPressed(isPressed: Bool) {
        if isPressed {
            let date = Date()
            touchDownDate = date

            DispatchQueue.main.asyncAfter(deadline: .now() + max(duration, 0)) {
                if date == touchDownDate {
                    disabled = true

                    DispatchQueue.main.async {
                        disabled = false
                    }
                }
            }
        } else {
            touchDownDate = nil
            disabled = false
        }
    }
}

This requires iOS 14 as it uses the onChange modifier, however I imagine this could be rewritten for iOS 13 by using onPreferenceChange instead.

4      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.