I am currently trying to build an application somewhat similar to procreate in SwiftUI (much less sophisticated of course ;) ).
My biggest issue is navigation right now. I want to use a 2-Finger pan gesture to navigate around the canvas. I have done plenty of research and I have not yet found a solution on how to implement this with SwiftUI gestures.
My current solution is using a UIViewRepresentable with an attached gesture listener. Unfortunately, this breaks all Gesture recognisers in subviews (which is a lot of them, and I need them all.
The current implementation is this here:
struct TwoFingerNavigationView: UIViewRepresentable {
var draggedCallback: ((CGPoint) -> Void)
var dragEndedCallback: (() -> Void)
class Coordinator: NSObject {
var draggedCallback: ((CGPoint) -> Void)
var dragEndedCallback: (() -> Void)
init(draggedCallback: @escaping ((CGPoint) -> Void),
dragEndedCallback: @escaping (() -> Void)) {
self.draggedCallback = draggedCallback
self.dragEndedCallback = dragEndedCallback
}
@objc func dragged(gesture: UIPanGestureRecognizer) {
if gesture.state == .ended {
self.dragEndedCallback()
} else {
self.draggedCallback(gesture.translation(in: gesture.view))
}
}
}
func makeUIView(context: UIViewRepresentableContext<TwoFingerNavigationView>) -> TwoFingerNavigationView.UIViewType {
let view = UIView(frame: .zero)
let gesture = UIPanGestureRecognizer(target: context.coordinator,
action: #selector(Coordinator.dragged))
gesture.minimumNumberOfTouches = 2
gesture.maximumNumberOfTouches = 2
view.addGestureRecognizer(gesture)
return view
}
func makeCoordinator() -> TwoFingerNavigationView.Coordinator {
return Coordinator(draggedCallback: self.draggedCallback,
dragEndedCallback: self.dragEndedCallback)
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<TwoFingerNavigationView>) {
}
}
The usage is as follows:
ZStack {
OtherView()
.gesture() // This will break with this "hack"
TwoFingerNavigationView(draggedCallback: { translation in
var newLocation = startLocation ?? location
newLocation.x += (translation.x / scale)
newLocation.y += (translation.y / scale)
self.location = newLocation
}, dragEndedCallback: {
startLocation = location
})
}
Does anyone have an idea how I could either replicate the functionality with SwiftUI gesture recognisers, or make it work with the UIKit implementation without breaking the gestures in all the children?
Any help and pointers are highly appreciated!
I don't have a solution for this exact problem, but I found a solution that worked out for me so far.
Since I was having massive performance hits thanks to basing my app on SwiftUI (can't handle high view counts very well), I had to make the hard decision to switch to SpriteKit.
Here is my current solution: I applied an overall touch recognizer that just captures all touch events that happen, and I do the interpretation of them myself now. I then pass the events back through callbacks that I use to feed the information to the appropriate views. I will expand this to capture taps on specific objects instead of just drag gestures, but so far, this is working fine.
class TouchScene: SKScene {
fileprivate let cameraNode = SKCameraNode()
var world: SKSpriteNode = SKSpriteNode(color: .clear, size: .zero)
fileprivate var previousPosition: CGPoint? = nil
fileprivate var touchesToIgnore: Int = 10
fileprivate var touchesIgnored: [CGPoint] = []
fileprivate var isDrawing: Bool = false
fileprivate var touchBegan: ((CGPoint) -> Void)? = nil
fileprivate var touchMoved: ((CGPoint) -> Void)? = nil
fileprivate var touchEnded: ((CGPoint) -> Void)? = nil
fileprivate var panBegan: ((CGPoint) -> Void)? = nil
fileprivate var panMoved: ((CGPoint) -> Void)? = nil
fileprivate var panEnded: ((CGPoint) -> Void)? = nil
init(documentSize: CGSize,
touchBegan: ((CGPoint) -> Void)? = nil,
touchMoved: ((CGPoint) -> Void)? = nil,
touchEnded: ((CGPoint) -> Void)? = nil,
panBegan: ((CGPoint) -> Void)? = nil,
panMoved: ((CGPoint) -> Void)? = nil,
panEnded: ((CGPoint) -> Void)? = nil) {
world = SKSpriteNode(color: .clear, size: documentSize)
super.init()
self.touchBegan = touchBegan
self.touchMoved = touchMoved
self.touchEnded = touchEnded
self.panBegan = panBegan
self.panMoved = panMoved
self.panEnded = panEnded
}
override init(size: CGSize) {
super.init(size: size)
addChild(world)
self.addChild(cameraNode)
self.camera = cameraNode
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override var isUserInteractionEnabled: Bool {
get { return true }
set { }
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let locations = locationsFor(touches: touches)
switch touches.count {
case 1: touchBegan?(locations[0].local)
default: panBegan?(locations[0].local)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let locations = locationsFor(touches: touches)
switch touches.count {
case 1:
guard touchesIgnored.count >= touchesToIgnore else { touchesIgnored.append(locations[0].local); return }
if touchesIgnored.count == touchesToIgnore {
touchesIgnored.forEach { ignoredPoint in
touchMoved?(ignoredPoint)
}
touchesIgnored.append(locations[0].local)
}
touchMoved?(locations[0].local)
default:
touchesIgnored = []
handlePanMoved(withTouches: touches)
panMoved?(locations[0].local)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let locations = locationsFor(touches: touches)
switch touches.count {
case 1:
touchEnded?(locations[0].local)
break
default: panEnded?(locations[0].local)
}
touchesIgnored = []
previousPosition = nil
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
let locations = locationsFor(touches: touches)
switch touches.count {
case 1: touchEnded?(locations[0].local)
default: panEnded?(locations[0].local)
}
touchesIgnored = []
previousPosition = nil
}
private func handlePanMoved(withTouches touches: Set<UITouch>) {
let newPosition = locationsFor(touches: touches)[0].global
defer { previousPosition = newPosition }
guard let oldPosition = previousPosition else { return }
world.position = world.position + (newPosition - oldPosition)
}
private func locationsFor(touches: Set<UITouch>) -> [(global: CGPoint, local: CGPoint)] {
var locations: [(global: CGPoint, local: CGPoint)] = []
for touch in touches {
let globalLocation = touch.location(in: self)
let localLocation = touch.location(in: world)
locations.append((global: globalLocation, local: localLocation))
}
return locations
}
}
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.