简体   繁体   中英

How to make a LongPressGesture that runs repeatedly while the button is still being held down in SwiftUI?

I'd like to run the code in the longPressGesture every 0.5 seconds while the button is being held down. Any ideas on how to implement this?

import SwiftUI

struct ViewName: View {
    var body: some View {
        VStack {
            Button(action: { } ) {
            Image(systemName: "chevron.left")
            .onTapGesture {
                //Run code for tap gesture here
            }
            .onLongPressGesture (minimumDuration: 0.5) {
                //Run this code every 0.5 seconds
            }
        }
    }
}

Oh boy, I'm not really an expert but I've had a similar problem (detecting pressing and releasing) recently and the solution I've found is less than elegant. I'd love if someone show a more elegant solution but here's my monstrosity:

import SwiftUI
import Combine

struct ContentView: View {

    @State private var ticker = Ticker()
    @State private var isPressed: Bool = false
    @State private var timePassed: TimeInterval?

    var body: some View {

        Button(action: {
            // Action when tapped
            NSLog("Tapped!")
        }) {
            Text(self.isPressed ? "Pressed for: \(String(format: "%0.1f", timePassed ?? 0))" : "Press and hold")
                .padding()
                .background(Capsule().fill(Color.yellow))
        }
        .onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { (value) in
                self.isPressed = value
                if value == true {
                    self.timePassed = 0
                    self.ticker.start(interval: 0.5)
                }

            }, perform: {})
            .onReceive(ticker.objectWillChange) { (_) in
                // Stop timer and reset the start date if the button in not pressed
                guard self.isPressed else {
                    self.ticker.stop()
                    return
                }

                // Your code here:
                self.timePassed = self.ticker.timeIntervalSinceStarted
            }
    }
}


/// Helper "ticker" that will publish regular "objectWillChange" messages
class Ticker: ObservableObject {

    var startedAt: Date = Date()

    var timeIntervalSinceStarted: TimeInterval {
        return Date().timeIntervalSince(startedAt)
    }

    private var timer: Timer?
    func start(interval: TimeInterval) {
        stop()
        startedAt = Date()
        timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
            self.objectWillChange.send()
        }
    }

    func stop() {
        timer?.invalidate()
    }

    deinit {
        timer?.invalidate()
    }

}

This requires an explanation:

  • onTapGesture() is not necessary here because that's what Button does by default, so just putting the code you need to run in the action block should be sufficient;
  • There is a limited number of gestures available in SwiftUI and, as far as I know, the only way to make new gestures is to combine existing ones;
  • There is no gesture that would continuously execute some code as long as the button is pressed, but LongPressGesture might be the closest to it. However, this gesture is recognized (and ends) when the allotted time expires but you want to detect the touch as long as it lasts, hence the minimumDuration: .infinity parameter;
  • LongPressGesture would also end when the touch has moved away a long enough distance, however, that's not how the Button works – you can wander away and return back and, as long as you've lifted the touch on top of the button view, the gesture will be recognized as a button press. We should replicate this behavior in our long press as well, hence maximumDistance: .infinity ;
  • With these parameters, the LongPressGesture will never be recognized, but there is a press parameter that now allows us to be notified when presses start and end;
  • Some sort of a timer could be used to execute a code block every so often; I've copied this "ticker" ObservableObject from somewhere. It has to be an ObservableObject because, that way, we can subscribe to it's updates within the View;
  • Now, when the button in pressed, we start the ticker;
  • When ticker ticks, we capture that with the onReceive() subscriber and that allows us to do something on every tick.

Something like that; again, I'd love someone to show me a better way:)

Good luck with your project!

–Baglan

You can do this by using timer. Make the timer starts when the user long pressed the image, and if the timer reaches 0, you can add two actions: 1. resetting the timer back to 0.5 seconds and 2.code you want to run every 0.5 seconds

    struct ContentView: View {
    @State var timeRemaining = 0.5
    let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
    @State var userIsPressing = false //detecting whether user is long pressing the screen

    var body: some View {
        VStack {
           Image(systemName: "chevron.left").onReceive(self.timer) { _ in
               if self.userIsPressing == true {
                 if self.timeRemaining > 0 {
                    self.timeRemaining -= 0.5
                  }
                //resetting the timer every 0.5 secdonds and executing code whenever //timer reaches 0

         if self.timeRemaining == 0 {
                print("execute this code")
                self.timeRemaining = 0.5
             }
            }
        }.gesture(LongPressGesture(minimumDuration: 0.5)
                       .onChanged() { _ in
                           //when longpressGesture started
                       self.userIsPressing = true
                       }
                       .onEnded() { _ in
                           //when longpressGesture ended
                       self.userIsPressing = false

                       }
                       )
               }
}
}

I simply cleaned up @Baglan 's "monstrosity" a bit this morning.

import Foundation
import SwiftUI

struct LongPressButton: View {
    @ObservedObject var timer = PressTimer()

    enum PressState {
        case inactive
        case pressing
        case finished
    }

    @State private var pressState = PressState.inactive

    var duration: Double = 2.0

    var body: some View {

        button
                .onLongPressGesture(minimumDuration: duration, maximumDistance: 50, pressing: { (value) in
                    if value == true {
                        /// Press has started
                        self.pressState = .pressing
                        print("start")
                        self.timer.start(duration)
                    } else {
                        /// Press has cancelled
                        self.pressState = .inactive
                        print("stop")
                        self.timer.stop()
                    }
                }, perform: {
                    /// Press has completed successfully
                    self.pressState = .finished
                    print("done")
                })
    }

    var button: some View {
        pressState == .pressing ? Text("Pressing - \(String(format: "%.0f", timer.percent))%")
                : Text("Start")
    }
}

class PressTimer: ObservableObject {

    @Published var percent: CGFloat = 0

    private var count: CGFloat = 0
    private let frameRateHz: CGFloat = 60
    private var durationSeconds: CGFloat = 2

    var timer: Timer?

    func start(_ duration: Double = 2.0) {
        self.durationSeconds = CGFloat(duration)
        let timerInterval: CGFloat = 1 / frameRateHz

        timer = Timer.scheduledTimer(withTimeInterval: Double(timerInterval), repeats: true, block: { _ in
            self.count += timerInterval
            self.percent = self.count / self.durationSeconds * 100
        })
    }

    func stop() {
        self.count = 0
        self.percent = 0
        self.timer?.invalidate()
        self.timer = nil
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM