Is there a way in SwiftUI to track multiple gestures at once? I want my one main view to be able to keep track of multiple fingers dragging at once.
ZStack {
Color.black
.edgesIgnoringSafeArea(.all)
.gesture(DragGesture(minimumDistance: 0)
.onChanged { (value) in
//some logic
}.onEnded { (value) in
//more logic
})
//other code
}
I have this code however I can only ever have one drag gesture being processed at a time. If one finger is dragging and then I try to add another one the first one stops.
I am trying to achieve an effect where multiple fingers are on screen at once. Each finger is dragging a circle simultaneously (one circle is following each finger).
I see simultaneous gestures on Apple's documentation, but this is referring to have one gesture trigger multiple blocks.
the solution (How to detect a tap gesture location in SwiftUI?) that uses UIViewRepresentable would work. Although its not the pure swiftUI solution (I've looked for a pure swiftUI solution but cannot find one and would be interested to see one). I've updated code to the below code, you would need to changed the numberOfTouchPoints = 5. and then use call back to get value into view.
struct TapView: UIViewRepresentable {
var tappedCallback: (() -> Void)
func makeUIView(context: UIViewRepresentableContext<TapView>) -> TapView.UIViewType {
let v = UIView(frame: .zero)
let gesture = NFingerGestureRecognizer(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:NFingerGestureRecognizer) {
for i in 0..<gesture.numberOfTouches{
print(gesture.location(ofTouch: i, in: gesture. view))
}
self.tappedCallback()
}
}
func makeCoordinator() -> TapView.Coordinator {
return Coordinator(tappedCallback:self.tappedCallback)
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<TapView>) {
}
}
class NFingerGestureRecognizer: UIGestureRecognizer {
var numberOfTouchPoints: Int = 2
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if self.state == .possible {
if (self.numberOfTouches == numberOfTouchPoints){ // we have a two finger interaction starting
self.state = .began
print("two finger drag - Start")
}
} else { // check to see if there are more touchs
if (self.numberOfTouches > numberOfTouchPoints){ // we have a two finger interaction starting
self.state = .failed
print("two finger drag - failed")
}
}
//printInternalTouches()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
if (self.state != .possible){
self.state = .changed // we are looking for two finger interaction
//printInternalTouches()
print("changed")
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
if numberOfTouches == 2 {
state = .ended
}
else {
state = .failed
}
//printInternalTouches()
print("ended")
}
private func printInternalTouches(){
// now print the internal touchUI elements - should be the better solution
print("Internal Locations")
for i in 0..<self.numberOfTouches{
print(self.location(ofTouch: i, in: self.view))
}
}
}
I combined Paul's answer with the outdated example from Apple to create a way to detect as many touches as you like.
Update 3: (commented code is for debug output)
import Foundation
import UIKit
class NFingerGestureRecognizer: UIGestureRecognizer {
var tappedCallback: (UITouch, CGPoint?) -> Void
var touchViews = [UITouch:CGPoint]()
init(target: Any?, tappedCallback: @escaping (UITouch, CGPoint?) -> ()) {
self.tappedCallback = tappedCallback
super.init(target: target, action: nil)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
for touch in touches {
let location = touch.location(in: touch.view)
// print("Start: (\(location.x)/\(location.y))")
touchViews[touch] = location
tappedCallback(touch, location)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
for touch in touches {
let newLocation = touch.location(in: touch.view)
// let oldLocation = touchViews[touch]!
// print("Move: (\(oldLocation.x)/\(oldLocation.y)) -> (\(newLocation.x)/\(newLocation.y))")
touchViews[touch] = newLocation
tappedCallback(touch, newLocation)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
for touch in touches {
// let oldLocation = touchViews[touch]!
// print("End: (\(oldLocation.x)/\(oldLocation.y))")
touchViews.removeValue(forKey: touch)
tappedCallback(touch, nil)
}
}
}
Update:
You need to wrap this into an UIViewRepresentable
to use it as a SwiftUI component:
import Foundation
import SwiftUI
struct TapView: UIViewRepresentable {
var tappedCallback: (UITouch, CGPoint?) -> Void
func makeUIView(context: UIViewRepresentableContext<TapView>) -> TapView.UIViewType {
let v = UIView(frame: .zero)
let gesture = NFingerGestureRecognizer(target: context.coordinator, tappedCallback: tappedCallback)
v.addGestureRecognizer(gesture)
return v
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<TapView>) {
// empty
}
}
Update 2:
A little example of how to use TapView
in SwiftUI:
var body: some View {
guard let img = UIImage(named: "background.png") else {
fatalError("Unable to load image")
}
return GeometryReader { geometry in
ZStack {
Image(uiImage: img)
.resizable()
.aspectRatio(geometry.size, contentMode: .fill)
.edgesIgnoringSafeArea(.all)
TapView { touch, optLocation in
// touch can be used as a dictionary key.
// optLocation is nil when the touch ends.
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With