How to Make a Swiftui Gesture That Keeps Running Code While the View Is Pressed

How do I make a SwiftUI gesture that keeps running code while the view is pressed

Use instead the following combination to track continuous pressing down

Tested with Xcode 11.4 / iOS 13.4

@GestureState var isLongPress = false // will be true till tap hold

var plusLongPress: some Gesture {
LongPressGesture(minimumDuration: 1).sequenced(before:
DragGesture(minimumDistance: 0, coordinateSpace:
.local)).updating($isLongPress) { value, state, transaction in
switch value {
case .second(true, nil):
state = true
// side effect here if needed
default:
break
}
}
}

How to run function when long press gesture detected and stopped?

I could solve my problem, hope it's useful for some of you:

@State private var isPressingDown: Bool = false

Image(systemName: "mic.circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 70)
.foregroundColor(Color.blue)
.onLongPressGesture(minimumDuration: 0.3){
self.isPressingDown = true
print("started")
}
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onEnded{ _ in
if self.isPressingDown{
self.isPressingDown = false
print("ended")
}
}
)

SwiftUI: Longpress Gesture Hold for only 1 Second

here is a very basic approach that you can build on, based on the code in:

https://adampaxton.com/make-a-press-and-hold-fast-forward-button-in-swiftui/

struct IgnitionDriveView: View {

@State private var timer: Timer?
@State var isLongPressD = false
@State var isLongPressR = false

@State private var showDriveAlert = true
@State private var showOutOfGasAlert = false
@State var distanceCovered: Float = 0.0

private func circleShape(isPressed: Binding<Bool>) -> some View {
Button(action: {
if isPressed.wrappedValue {
isPressed.wrappedValue.toggle()
timer?.invalidate()
}
}) {
ZStack {
Circle().strokeBorder(style: StrokeStyle(lineWidth: 2))
Circle().fill(isPressed.wrappedValue ? .white : .red)
}.frame(width: 100, height: 100, alignment: .center)
}
}

var body: some View {

VStack(alignment: .leading) {
Text("Distance Covered in Km: \(distanceCovered)").font(.headline)
ProgressView(value: distanceCovered > 0 ? distanceCovered : 0, total: 1000).frame(height: 40)

HStack {
ZStack {
circleShape(isPressed: $isLongPressD)
.simultaneousGesture(LongPressGesture(minimumDuration: 0.2).onEnded { _ in
isLongPressD = true
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
if distanceCovered < 1000 {
distanceCovered += 10
} else {
showOutOfGasAlert = true
}
})
})

Text("D").bold().padding().foregroundColor(.green).font(.title)
}.foregroundColor(.green)

Spacer()

ZStack {
circleShape(isPressed: $isLongPressR)
.simultaneousGesture(LongPressGesture(minimumDuration: 0.2).onEnded { _ in
isLongPressR = true
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
if distanceCovered > 0 {
distanceCovered -= 10
}
})
})

Text("R").bold().padding().foregroundColor(.blue).font(.title)
}.foregroundColor(.green)

}.padding()
}.alert("Press D to Drive and R to Reverse", isPresented: $showDriveAlert) {
Button("Okay") { showDriveAlert = false }
}.alert("You ran out of Gas, Reverse to Gas Station", isPresented: $showOutOfGasAlert) {
Button("Sucks, but fine!") { showOutOfGasAlert = false }
}
.padding()
}
}

SwiftUI run code periodically while button is being held down; run different code when it is just tapped?

Here is a solution - to get continuous pressing it needs to combine long press gesture with sequenced drag and add timer in handlers.

Updated: Tested with Xcode 11.4 / iOS 13.4 (in Preview & Simulator)

struct TimeEventGeneratorView: View {
var callback: () -> Void
private let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()

var body: some View {
Color.clear
.onReceive(self.timer) { _ in
self.callback()
}
}
}

struct TestContinuousPress: View {

@GestureState var pressingState = false // will be true till tap hold
var pressingGesture: some Gesture {
LongPressGesture(minimumDuration: 0.5).sequenced(before:
DragGesture(minimumDistance: 0, coordinateSpace:
.local)).updating($pressingState) { value, state, transaction in
switch value {
case .second(true, nil):
state = true
default:
break
}
}.onEnded { _ in
}
}

var body: some View {
VStack {
Image(systemName: "chevron.left")
.background(Group { if self.pressingState { TimeEventGeneratorView {
print(">>>> pressing: \(Date())")
}}})
.gesture(TapGesture().onEnded {
print("> just tap ")
})
.gesture(pressingGesture)
}
}
}

How to detect and take action when a button is being pressed in SwiftUI

As soon as native SwiftUI does not allow now what you want to achieve, I'd recommend the following approach, which is valid and manageable and, so, reliable.

The demo shows simplified code based on using UIGestureRecongnizer/UIViewRepresentable, which can be easily extended (eg. if you want to intercept touchesCanceled, click count, etc.)

import SwiftUI
import UIKit

class MyTapGesture : UITapGestureRecognizer {

var didBeginTouch: (()->Void)?
var didEndTouch: (()->Void)?

init(target: Any?, action: Selector?, didBeginTouch: (()->Void)? = nil, didEndTouch: (()->Void)? = nil) {
super.init(target: target, action: action)
self.didBeginTouch = didBeginTouch
self.didEndTouch = didEndTouch
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
self.didBeginTouch?()
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.didEndTouch?()
}
}

struct TouchesHandler: UIViewRepresentable {
var didBeginTouch: (()->Void)?
var didEndTouch: (()->Void)?

func makeUIView(context: UIViewRepresentableContext<TouchesHandler>) -> UIView {
let view = UIView(frame: .zero)
view.isUserInteractionEnabled = true
view.addGestureRecognizer(context.coordinator.makeGesture(didBegin: didBeginTouch, didEnd: didEndTouch))
return view;
}

func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<TouchesHandler>) {
}

func makeCoordinator() -> Coordinator {
return Coordinator()
}

class Coordinator {
@objc
func action(_ sender: Any?) {
print("Tapped!")
}

func makeGesture(didBegin: (()->Void)?, didEnd: (()->Void)?) -> MyTapGesture {
MyTapGesture(target: self, action: #selector(self.action(_:)), didBeginTouch: didBegin, didEndTouch: didEnd)
}
}
typealias UIViewType = UIView
}

struct TestCustomTapGesture: View {
var body: some View {
Text("Hello, World!")
.padding()
.background(Color.yellow)
.overlay(TouchesHandler(didBeginTouch: {
print(">> did begin")
}, didEndTouch: {
print("<< did end")
}))
}
}

struct TestCustomTapGesture_Previews: PreviewProvider {
static var previews: some View {
TestCustomTapGesture()
}
}

How to run code when swiftui gesture recognizers begin

There is special @GestureState, which can be used for such purpose. So, here is possible approach

struct TestGestureBegin: View {

enum Progress {
case inactive
case started
case changed
}
@GestureState private var gestureState: Progress = .inactive // initial & reset value

var body: some View {
VStack {
Text("Drag over me!")
}
.frame(width: 200, height: 200)
.background(Color.yellow)
.gesture(DragGesture(minimumDistance: 0)
.updating($gestureState, body: { (value, state, transaction) in
switch state {
case .inactive:
state = .started
print("> started")
case .started:
state = .changed
print(">> just changed")
case .changed:
print(">>> changing")
}
})
.onEnded { value in
print("x ended")
}
)
}
}

SwiftUI LongPressGesture takes too long to recognize when TapGesture also present

To having some multi gesture that fits every ones needs in projects, Apple has nothing offer than normal gesture, mixing them together to reach the wished gesture some times get tricky, here is a salvation, working without issue or bug!

Here a custom zero issue gesture called interactionReader, we can apply it to any View. for having LongPressGesture and TapGesture in the same time.


Sample Image



import SwiftUI

struct ContentView: View {

var body: some View {

Circle()
.fill(Color.yellow)
.frame(width: 150, height: 150)
.interactionReader(longPressSensitivity: 250, tapAction: tapAction, longPressAction: longPressAction, scaleEffect: true)
.animation(Animation.easeInOut(duration: 0.2))

}

func tapAction() { print("tap action!") }

func longPressAction() { print("longPress action!") }

}


struct InteractionReaderViewModifier: ViewModifier {

var longPressSensitivity: Int
var tapAction: () -> Void
var longPressAction: () -> Void
var scaleEffect: Bool = true

@State private var isPressing: Bool = Bool()
@State private var currentDismissId: DispatchTime = DispatchTime.now()
@State private var lastInteractionKind: String = String()

func body(content: Content) -> some View {

let processedContent = content
.gesture(gesture)
.onChange(of: isPressing) { newValue in

currentDismissId = DispatchTime.now() + .milliseconds(longPressSensitivity)
let dismissId: DispatchTime = currentDismissId

if isPressing {

DispatchQueue.main.asyncAfter(deadline: dismissId) {

if isPressing { if (dismissId == currentDismissId) { lastInteractionKind = "longPress"; longPressAction() } }

}

}
else {

if (lastInteractionKind != "longPress") { lastInteractionKind = "tap"; tapAction() }

DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {lastInteractionKind = "none"}


}

}

return Group {

if scaleEffect { processedContent.scaleEffect(lastInteractionKind == "longPress" ? 1.5: (lastInteractionKind == "tap" ? 0.8 : 1.0 )) }
else { processedContent }

}

}

var gesture: some Gesture {

DragGesture(minimumDistance: 0.0, coordinateSpace: .local)
.onChanged() { _ in if !isPressing { isPressing = true } }
.onEnded() { _ in isPressing = false }

}

}


extension View {

func interactionReader(longPressSensitivity: Int, tapAction: @escaping () -> Void, longPressAction: @escaping () -> Void, scaleEffect: Bool = true) -> some View {

return self.modifier(InteractionReaderViewModifier(longPressSensitivity: longPressSensitivity, tapAction: tapAction, longPressAction: longPressAction, scaleEffect: scaleEffect))

}

}


Related Topics



Leave a reply



Submit