Использование SwiftUI с несколькими UIGestureRecognizers?

ios #swiftui #uikit #uigesturerecognizer

#iOS #swiftui #uikit #uigesturerecognizer

Вопрос:

В настоящее время я пытаюсь написать приложение, которое использует навигацию, аналогичную таким приложениям, как Affinity или Procreate, где вы используете одно касание / перетаскивание для рисования, ретуширования и взаимодействия, а также жесты двумя пальцами для навигации по холсту.

Я создаю приложение, используя SwiftUI в качестве основной платформы и включаю UIKit по мере необходимости.

К сожалению, SwiftUI пока не позволяет выполнять сложные жесты, подобные описанным, но UIKit обычно это делает. Поэтому я вернулся к использованию UIKit в качестве средств распознавания жестов вместо того, чтобы полагаться на жесты SwiftUI. Однако это вызывает проблему, заключающуюся в том, что будет вызываться только самый верхний распознаватель жестов. Я надеялся на распознавание нескольких одновременных жестов , как показано здесь, но, к сожалению, SwiftUI, похоже, вызывает проблемы с UIViewRepresentables.

Может кто-нибудь помочь мне найти решение этой проблемы?

Важно: я делаю это с двумя отдельными представлениями, потому что в конечном итоге они будут использоваться в разных представлениях. Однако в примере они отображаются в одном и том же представлении для демонстрационных целей.

Использование:

 ZStack {
    DragGestureView { point in
        print("One Finger")
    } dragEndedCallback: {
        print("One Finger Ended")
    }

    TwoFingerNavigationView { point in
        viewStore.send(.dragChanged(point))
        print("Two Fingers")
    } dragEndedCallback: {
        viewStore.send(.dragEnded)
        print("Two Fingers Ended")
    } pinchedCallback: { value in
        viewStore.send(.magnificationChanged(value))
    } pinchEndedCallback: {
        viewStore.send(.magnificationEnded)
    }

    content()
        .position(viewStore.location)
        .scaleEffect(viewStore.scale * viewStore.offsetScale)
}
 

DragGestureView

 public struct DragGestureView: UIViewRepresentable {
    let delegate = GestureRecognizerDelegate()
    var draggedCallback: ((CGPoint) -> Void)
    var dragEndedCallback: (() -> Void)

    public init(draggedCallback: @escaping ((CGPoint) -> Void), dragEndedCallback: @escaping (() -> Void)) {
        self.draggedCallback = draggedCallback
        self.dragEndedCallback = dragEndedCallback
    }

    public class Coordinator: NSObject {
        var draggedCallback: ((CGPoint) -> Void)
        var dragEndedCallback: (() -> Void)

        public 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.location(in: gesture.view))
            }
        }
    }

    class GestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return true
        }
    }

    public func makeUIView(context: UIViewRepresentableContext<DragGestureView>) -> DragGestureView.UIViewType {
        let view = UIView(frame: .zero)
        let gesture = UIPanGestureRecognizer(target: context.coordinator,
                                             action: #selector(Coordinator.dragged))
        gesture.minimumNumberOfTouches = 1
        gesture.maximumNumberOfTouches = 1
        gesture.delegate = delegate
        view.addGestureRecognizer(gesture)
        return view
    }

    public func makeCoordinator() -> DragGestureView.Coordinator {
        return Coordinator(draggedCallback: self.draggedCallback,
                           dragEndedCallback: self.dragEndedCallback)
    }

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

TwoFingerNavigationView

 struct TwoFingerNavigationView: UIViewRepresentable {
    let delegate = GestureRecognizerDelegate()

    var draggedCallback: ((CGPoint) -> Void)
    var dragEndedCallback: (() -> Void)
    var pinchedCallback: ((CGFloat) -> Void)
    var pinchEndedCallback: (() -> Void)

    class Coordinator: NSObject {
        var draggedCallback: ((CGPoint) -> Void)
        var dragEndedCallback: (() -> Void)
        var pinchedCallback: ((CGFloat) -> Void)
        var pinchEndedCallback: (() -> Void)

        var startingDistance: CGFloat? = nil
        var isMagnifying = false
        var startingMagnification: CGFloat? = nil
        var newMagnification: CGFloat = 1.0

        init(draggedCallback: @escaping ((CGPoint) -> Void),
             dragEndedCallback: @escaping (() -> Void),
             pinchedCallback: @escaping ((CGFloat) -> Void),
             pinchEndedCallback: @escaping (() -> Void)) {
            self.draggedCallback = draggedCallback
            self.dragEndedCallback = dragEndedCallback
            self.pinchedCallback = pinchedCallback
            self.pinchEndedCallback = pinchEndedCallback
        }

        @objc func dragged(gesture: UIPanGestureRecognizer) {
            if gesture.state == .ended {
                self.dragEndedCallback()
                self.pinchEndedCallback()
                startingDistance = nil
                isMagnifying = false
                startingMagnification = nil
                newMagnification = 1.0
            } else {
                self.draggedCallback(gesture.translation(in: gesture.view) / (newMagnification))
            }

            var touchLocations: [CGPoint] = []
            for i in 0..<gesture.numberOfTouches{
                touchLocations.append(gesture.location(ofTouch: i, in: gesture.view))
            }

            if touchLocations.count == 2 {
                let distanceVector = (touchLocations[0] - touchLocations[1])
                let distance = sqrt(distanceVector.x * distanceVector.x   distanceVector.y * distanceVector.y)

                guard startingDistance != nil else { startingDistance = distance; return }
                guard distance - startingDistance! > 30 || distance - startingDistance! < -30 || isMagnifying else { return }
                isMagnifying = true;

                if startingMagnification == nil {
                    startingMagnification = distance / 100
                    pinchedCallback(1)
                } else {
                    let magnification = distance / 100
                    newMagnification = magnification / startingMagnification!
                    pinchedCallback(newMagnification)
                }
            }
        }
    }

    class GestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return true
        }
    }

    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
        gesture.delegate = delegate
        view.addGestureRecognizer(gesture)
        return view
    }

    func makeCoordinator() -> TwoFingerNavigationView.Coordinator {
        return Coordinator(draggedCallback: self.draggedCallback,
                           dragEndedCallback: self.dragEndedCallback,
                           pinchedCallback: self.pinchedCallback,
                           pinchEndedCallback: self.pinchEndedCallback)
    }

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

Комментарии:

1. Вам повезло — вы можете просто поместить оба средства распознавания жестов в один класс, который реализует UIViewRepresentable. У меня это работает с пользовательским распознавателем жестов и стандартным UITapGestureRecognizer. В моем случае мне нужны кнопки SwiftUI для работы с распознавателями жестов UIKit, и когда я добавляю фон с пользовательским представлением, в котором используется пользовательский UIGestureRecognizer, все кнопки перестают работать (и другие элементы управления SwiftUI тоже).

Ответ №1:

Я не нашел никакого подтверждения своим выводам в официальной документации, но, похоже, жесты SwiftUI работают иначе, чем UIKit. В общем случае — для сложных случаев вы можете использовать либо один, либо другой.

  • Если у вас есть элементы UIKit, например, кнопки в представлении, то при добавлении жестов SwiftUI они «получают все касания», и элементы UIKit перестают работать (ничего не получают).
  • Если вы не добавили никаких жестов SwiftUI в представление, использующее элементы UIKit, тогда все материалы UIKit работают нормально. Вот почему пользовательские представления UIKit отлично работают в SwiftUI.

Если вы добавляете представления SwiftUI друг к другу с помощью любых жестов, то, как вы упомянули, работают только жесты просмотра верхнего уровня. В UIKit то же самое: 1) сначала hitTests находит все представления для данной точки, затем 2) система собирает все UIGestureRecognizers для этих представлений, затем 3) пытается передать касания в систему распознавания жестов и проверяет, какие распознаватели жестов получат касания, а какие должны завершиться неудачей. Итак, в вашем случае у вас нет нижележащего представления DragGestureView в иерархии, собранной на шаге 1). Итак, работают только распознаватели жестов из TwoFingerNavigationView.

Решение для вас — поместить оба средства распознавания жестов в один пользовательский интерфейс. Они появятся как на шаге 2), так и после вашей реализации будут работать одновременно.

Комментарии:

1. Приятно! Это потрясающе! Я обязательно попробую! Большое вам спасибо!