How to Detect a Swiftui Touchdown Event with No Movement or Duration

How do you detect a SwiftUI touchDown event with no movement or duration?

If you combine the code from these two questions:

How to detect a tap gesture location in SwiftUI?

UITapGestureRecognizer - make it work on touch down, not touch up?

You can make something like this:

ZStack {
Text("Test")
TapView {
print("Tapped")
}
}
struct TapView: UIViewRepresentable {
var tappedCallback: (() -> Void)

func makeUIView(context: UIViewRepresentableContext<TapView>) -> TapView.UIViewType {
let v = UIView(frame: .zero)
let gesture = SingleTouchDownGestureRecognizer(target: context.coordinator,
action: #selector(Coordinator.tapped))
v.addGestureRecognizer(gesture)
return v
}

class Coordinator: NSObject {
var tappedCallback: (() -> Void)

init(tappedCallback: @escaping (() -> Void)) {
self.tappedCallback = tappedCallback
}

@objc func tapped(gesture:UITapGestureRecognizer) {
self.tappedCallback()
}
}

func makeCoordinator() -> TapView.Coordinator {
return Coordinator(tappedCallback:self.tappedCallback)
}

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

class SingleTouchDownGestureRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if self.state == .possible {
self.state = .recognized
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
self.state = .failed
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
self.state = .failed
}
}

There's definitely some abstractions we can make so that the usage is more like the other SwiftUI Gestures, but this is a start. Hopefully Apple builds in support for this at some point.

Capture touchDown location of onLongPressGesture in swiftUI?

Here is a demo of possible approach. It needs a combination of two gestures: LongPress to detect long press and Drag to detect location.

Tested with Xcode 12 / iOS 14. (on below systems it might be needed to add self. to some properties usage)

demo

struct ExampleView: View {
@State var showCustomContextMenu = false
@State var longPressLocation = CGPoint.zero

var body: some View {
Rectangle()
.foregroundColor(Color.green)
.frame(width: 100.0, height: 100.0)
.onTapGesture { showCustomContextMenu = false } // just for demo
.gesture(LongPressGesture(minimumDuration: 1).sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .local))
.onEnded { value in
switch value {
case .second(true, let drag):
longPressLocation = drag?.location ?? .zero // capture location !!
showCustomContextMenu = true
default:
break
}
})
.overlay(
Rectangle()
.foregroundColor(Color.red)
.frame(width: 50.0, height: 50.0)
.position(longPressLocation)
.opacity( (showCustomContextMenu) ? 1 : 0 )
.allowsHitTesting(false)
)
}
}

SwiftUI TapGesture onStart / TouchDown

If by pure SwiftUI then only indirectly for now.

Here is an approach. Tested with Xcode 11.4.

Note: minimumDistance: 0.0 below is important !!

MyView()
.gesture(DragGesture(minimumDistance: 0.0, coordinateSpace: .global)
.onChanged { _ in
print(">> touch down") // additional conditions might be here
}
.onEnded { _ in
print("<< touch up")
}
)

Detect single/multi-finger gesture Swiftui

I am able to find my solution using UIKit UIView. Actually made container view(UIVIewRepresentable) which implements the PanGesture. I have used this view to to hold SwiftUI view. I can easily find and able to handle Panning behaviour.

iOS Detect tap down and touch up of a UIView

A Gesture Recognizer is probably overkill for what you want. You probably just want to use a combination of -touchesBegan:withEvent: and -touchesEnded:withEvent:.

This is flawed, but it should give you and idea of what you want to do.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
self.touchDown = YES;
self.backgroundColor = [UIColor redColor];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// Triggered when touch is released
if (self.isTouchDown) {
self.backgroundColor = [UIColor whiteColor];
self.touchDown = NO;
}
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
// Triggered if touch leaves view
if (self.isTouchDown) {
self.backgroundColor = [UIColor whiteColor];
self.touchDown = NO;
}
}

This code should go in a custom subclass of UIView that you create. Then use this custom view type instead of UIView and you'll get touch handling.

Is there a SwiftUI drag enter gesture?

I ended up intercepting touches in an overlay view, then highlight items when touch location is contained in its bounds. I tried both onDrop and using UIKit gestures/actions, but none of them came without drawbacks. So I opted to implement George's comment above instead.

Sample Image

I incorporated a model object where I store the frames on every update, so highlight effect can adopt dynamically. It makes the solution animation proof, while picker also can be displayed anywhere on the screen.

import SwiftUI


class Model: ObservableObject {

let coordinateSpace = "CoordinateSpace"

@Published var isDragged = false
@Published var highlightedNumber: Int? = nil
@Published var selectedNumber: Int? = nil

/// Frames for individual picker items (from their respective `GeometryReader`).
private var framesForNumbers: [Int: CGRect] = [:]

func update(frame: CGRect, for number: Int) {
framesForNumbers[number] = frame
}

/// Updates `highlightedNumber` according drag location.
func update(dragLocation: CGPoint) {

// Lookup if any frame contains drag location.
for (eachNumber, eachFrame) in framesForNumbers {
if eachFrame.contains(dragLocation) {

// Publish.
self.highlightedNumber = eachNumber
return
}
}

// Reset otherwise.
self.highlightedNumber = nil
}

/// Updates `highlightedNumber` and `selectedNumber` according drop location.
func update(isDragged: Bool) {

// Publish.
self.isDragged = isDragged

if isDragged == false,
let highlightedNumber = self.highlightedNumber {

// Publish.
self.selectedNumber = highlightedNumber
self.highlightedNumber = nil
}
}
}

struct ContentView: View {

@StateObject var model = Model()

var body: some View {
ZStack {
TouchesView(model: model, isDragged: $model.isDragged)
CanvasView(number: $model.selectedNumber)
.allowsHitTesting(false)
PickerView(model: model, highlightedNumber: $model.highlightedNumber)
.allowsHitTesting(false)
}
.ignoresSafeArea()
}
}

/// Handles drag interactions and updates model accordingly.
struct TouchesView: View {

var model: Model
@Binding var isDragged: Bool

var body: some View {
Rectangle()
.foregroundColor(isDragged ? .orange : .yellow)
.coordinateSpace(name: model.coordinateSpace)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
model.update(dragLocation: value.location)
model.update(isDragged: true)
}
.onEnded { state in
model.update(dragLocation: state.location)
model.update(isDragged: false)
}
)
}
}

/// Shows the selected number.
struct CanvasView: View {

@Binding var number: Int?

var body: some View {
VStack {
Text(number.string)
.font(.system(size: 100, weight: .bold))
.foregroundColor(.white)
.offset(y: -50)
}
}
}

/// Displays a picker to select number items from.
struct PickerView: View {

var model: Model
@Binding var highlightedNumber: Int?

var body: some View {
HStack(spacing: 5) {
PickerItemView(number: 1, model: model, highlightedNumber: $highlightedNumber)
PickerItemView(number: 2, model: model, highlightedNumber: $highlightedNumber)
PickerItemView(number: 3, model: model, highlightedNumber: $highlightedNumber)
}
.opacity(model.isDragged ? 1 : 0)
.scaleEffect(model.isDragged ? 1 : 0.5, anchor: .top)
.blur(radius: model.isDragged ? 0 : 10)
.animation(.spring(response: 0.15, dampingFraction: 0.4, blendDuration: 0.5))
}
}

/// Shows a number item (also highlights it when `highlightedNumber` matches).
struct PickerItemView: View {

let number: Int
var model: Model
@Binding var highlightedNumber: Int?

var body: some View {
Text(String(number))
.font(.system(size: 25, weight: .bold))
.foregroundColor(isHighlighted ? .orange : .white)
.frame(width: 50, height: 50)
.background(isHighlighted ? Color.white : Color.orange)
.cornerRadius(25)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white, lineWidth: 2)
)
.overlay(GeometryReader { geometry -> Color in
self.model.update(
frame: geometry.frame(in: .named(self.model.coordinateSpace)),
for: self.number
)
return Color.clear
})
.animation(.none)
}
}

extension PickerItemView {

var isHighlighted: Bool {
self.highlightedNumber == self.number
}
}

fileprivate extension Optional where Wrapped == Int {

var string: String {
if let number = self {
return String(number)
} else {
return ""
}
}
}

struct PrototypeView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Only thing I dislike about this is that this method is esentially the same I did 15 years ago in a Flash app. I was hoping to something less "manual".



Related Topics



Leave a reply



Submit