简体   繁体   中英

SwiftUI 2-Finger Pan Gesture

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.

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