NSPanGesture 不适用于 AppKit,但 UIPanGesture 适用于 iOS(多平台应用程序)

问题描述 投票:0回答:1

我发现了一个旧的 SwiftUI 库,我试图将其移植到 MacOS。您可以在这里找到完整的存储库:https://github.com/cyrilzakka/SlidingRuler。本质上,它是一个实现了股票粘性的滑动标尺。屏幕截图如下: [![在此处输入图像描述][1]][1]

照原样,代码在 iOS 上完美运行,我可以左右拖动滑块来修改值。在 macOS (AppKit) 上,虽然 UI 看起来不错,但拖动滑块似乎不起作用。

这里

HorizontalPanGesture.swift

import SwiftUI
import CoreGeometry

#if canImport(UIKit)
struct HorizontalDragGestureValue {
    let state: UIGestureRecognizer.State
    let translation: CGSize
    let velocity: CGFloat
    let startLocation: CGPoint
    let location: CGPoint
}

protocol HorizontalPanGestureReceiverViewDelegate: AnyObject {
    func viewTouchedWithoutPan(_ view: UIView)
}

class HorizontalPanGestureReceiverView: UIView {
    weak var delegate: HorizontalPanGestureReceiverViewDelegate?

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        delegate?.viewTouchedWithoutPan(self)
    }
}

extension View {
    func onHorizontalDragGesture(initialTouch: @escaping () -> () = { },
                                 prematureEnd: @escaping () -> () = { },
                                 perform action: @escaping (HorizontalDragGestureValue) -> ()) -> some View {
        self.overlay(HorizontalPanGesture(beginTouch: initialTouch, prematureEnd: prematureEnd, action: action))
    }
}

private struct HorizontalPanGesture: UIViewRepresentable {
    typealias Action = (HorizontalDragGestureValue) -> ()
    
    class Coordinator: NSObject, UIGestureRecognizerDelegate, HorizontalPanGestureReceiverViewDelegate {
        private let beginTouch: () -> ()
        private let prematureEnd: () -> ()
        private let action: Action
        weak var view: UIView?
        
        init(_ beginTouch: @escaping () -> () = { }, _ prematureEnd: @escaping () -> () = { }, _ action: @escaping Action) {
            self.beginTouch = beginTouch
            self.prematureEnd = prematureEnd
            self.action = action
        }
        
        @objc func panGestureHandler(_ gesture: UIPanGestureRecognizer) {
            let translation = gesture.translation(in: view)
            let velocity = gesture.velocity(in: view)
            let location = gesture.location(in: view)
            let startLocation = location - translation

            let value = HorizontalDragGestureValue(state: gesture.state,
                                                   translation: .init(horizontal: translation.x),
                                                   velocity: velocity.x,
                                                   startLocation: startLocation,
                                                   location: location)
            self.action(value)
        }
        
        func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
            guard let pgr = gestureRecognizer as? UIPanGestureRecognizer else { return false }
            let velocity = pgr.velocity(in: view)
            return abs(velocity.x) > abs(velocity.y)
        }

        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive event: UIEvent) -> Bool {
            beginTouch()
            return true
        }

        func viewTouchedWithoutPan(_ view: UIView) {
            prematureEnd()
        }
    }

    @Environment(\.slidingRulerStyle) private var style
    
    let beginTouch: () -> ()
    let prematureEnd: () -> ()
    let action: Action
    
    func makeCoordinator() -> Coordinator {
        .init(beginTouch, prematureEnd, action)
    }
    
    func makeUIView(context: Context) -> UIView {
        let view = HorizontalPanGestureReceiverView(frame: .init(size: .init(square: 42)))
        let pgr = UIPanGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.panGestureHandler(_:)))
        view.delegate = context.coordinator
        pgr.delegate = context.coordinator
        view.addGestureRecognizer(pgr)
        context.coordinator.view = view

        // Pointer interactions
        if #available(iOS 13.4, *), style.supportsPointerInteraction {
            pgr.allowedScrollTypesMask = .continuous
            view.addInteraction(UIPointerInteraction(delegate: context.coordinator))
        }

        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) { }
}

@available(iOS 13.4, *)
extension HorizontalPanGesture.Coordinator: UIPointerInteractionDelegate {
    func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
        .init(shape: .path(Pointers.standard), constrainedAxes: .vertical)
    }
}


#elseif canImport(AppKit)
struct HorizontalDragGestureValue {
    let state: NSGestureRecognizer.State
    let translation: CGSize
    let velocity: CGFloat
    let startLocation: CGPoint
    let location: CGPoint
}

protocol HorizontalPanGestureReceiverViewDelegate: AnyObject {
    func viewTouchedWithoutPan(_ view: NSView)
}

class HorizontalPanGestureReceiverView: NSView {
    weak var delegate: HorizontalPanGestureReceiverViewDelegate?
    
    override func touchesEnded(with event: NSEvent) {
        super.touchesEnded(with: event)
        delegate?.viewTouchedWithoutPan(self)
    }
}

extension View {
    func onHorizontalDragGesture(initialTouch: @escaping () -> () = { },
                                 prematureEnd: @escaping () -> () = { },
                                 perform action: @escaping (HorizontalDragGestureValue) -> ()) -> some View {
        self.overlay(HorizontalPanGesture(beginTouch: initialTouch, prematureEnd: prematureEnd, action: action))
    }
}

private struct HorizontalPanGesture: NSViewRepresentable {
    typealias Action = (HorizontalDragGestureValue) -> ()

    class Coordinator: NSObject, NSGestureRecognizerDelegate, HorizontalPanGestureReceiverViewDelegate {
        private let beginTouch: () -> ()
        private let prematureEnd: () -> ()
        private let action: Action
        weak var view: NSView?
        
        init(_ beginTouch: @escaping () -> () = { }, _ prematureEnd: @escaping () -> () = { }, _ action: @escaping Action) {
            self.beginTouch = beginTouch
            self.prematureEnd = prematureEnd
            self.action = action
        }
        
        @objc func panGestureHandler(_ gesture: NSPanGestureRecognizer) {
            print("PanGesture handler called")
            let translation = gesture.translation(in: view)
            let velocity = gesture.velocity(in: view)
            let location = gesture.location(in: view)
            let startLocation = location - translation

            let value = HorizontalDragGestureValue(state: gesture.state,
                                                   translation: .init(horizontal: translation.x),
                                                   velocity: velocity.x,
                                                   startLocation: startLocation,
                                                   location: location)
            self.action(value)
        }
        
        func gestureRecognizerShouldBegin(_ gestureRecognizer: NSGestureRecognizer) -> Bool {
            guard let pgr = gestureRecognizer as? NSPanGestureRecognizer else { return false }
            let velocity = pgr.velocity(in: view)
            return abs(velocity.x) > abs(velocity.y)
        }
        
        func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldReceive touch: NSTouch) -> Bool {
            beginTouch()
            return true
        }

        func viewTouchedWithoutPan(_ view: NSView) {
            prematureEnd()
        }
    }
    
    @Environment(\.slidingRulerStyle) private var style
    
    let beginTouch: () -> ()
    let prematureEnd: () -> ()
    let action: Action
    
    func makeCoordinator() -> Coordinator {
        .init(beginTouch, prematureEnd, action)
    }
    
    func makeNSView(context: Context) -> some NSView {
        let view = HorizontalPanGestureReceiverView(frame: .init(size: .init(square: 42)))
        let pgr = NSPanGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.panGestureHandler(_:)))
        view.delegate = context.coordinator
        pgr.delegate = context.coordinator
        view.addGestureRecognizer(pgr)
        context.coordinator.view = view

        return view
    }
    
    func updateNSView(_ nsView: NSViewType, context: Context) { }
}

#endif

以及如何使用:

public var body: some View {
        let renderedValue: CGFloat, renderedOffset: CGSize

        (renderedValue, renderedOffset) = renderingValues()

        return FlexibleWidthContainer {
            ZStack(alignment: .init(horizontal: .center, vertical: self.verticalCursorAlignment)) {
                Ruler(cells: self.cells, step: self.step, markOffset: self.markOffset, bounds: self.bounds, formatter: self.formatter)
                    .equatable()
                    .animation(nil)
                    .modifier(InfiniteOffsetEffect(offset: renderedOffset, maxOffset: self.cellWidthOverflow))
                self.style.makeCursorBody()
            }
        }

        .modifier(InfiniteMarkOffsetModifier(renderedValue, step: step))
        .propagateWidth(ControlWidthPreferenceKey.self)
        .onPreferenceChange(MarkOffsetPreferenceKey.self, storeValueIn: $markOffset)
        .onPreferenceChange(ControlWidthPreferenceKey.self, storeValueIn: $controlWidth) {
            self.updateCellsIfNeeded()
        }
        .transaction {
            if $0.animation != nil { $0.animation = .easeIn(duration: 0.1) }
        }
        .onHorizontalDragGesture(initialTouch: firstTouchHappened,
                                 prematureEnd: panGestureEndedPrematurely,
                                 perform: horizontalDragAction(withValue:))
    }

我似乎找不到哪里可能出错。

唯一其他潜在(但不太可能)有问题的部分可能是

CADisplayLink
,我必须在 macOS 上以不同的方式实现:

#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
import CoreVideo
#endif

struct VSynchedTimer {
    typealias Animations = (TimeInterval, TimeInterval) -> ()
    typealias Completion = (Bool) -> ()
    
    private let timer: SynchedTimer
    
    init(duration: TimeInterval, animations: @escaping Animations, completion: Completion? = nil) {
        self.timer = .init(duration, animations, completion)
    }
    
    func cancel() {
        timer.cancel()
    }
}


private final class SynchedTimer {
    private let duration: TimeInterval
    private let animationBlock: VSynchedTimer.Animations
    private let completionBlock: VSynchedTimer.Completion?
#if canImport(UIKit)
    private weak var displayLink: CADisplayLink?
#elseif canImport(AppKit)
    private var displayLink: CVDisplayLink?
#endif
    
    private var isRunning: Bool
    private let startTimeStamp: TimeInterval
    private var lastTimeStamp: TimeInterval
    
    deinit {
        cancel()
    }
    
    init(_ duration: TimeInterval, _ animations: @escaping VSynchedTimer.Animations, _ completion: VSynchedTimer.Completion? = nil) {
        self.duration = duration
        self.animationBlock = animations
        self.completionBlock = completion
        
        self.isRunning = true
        self.startTimeStamp = CACurrentMediaTime()
        self.lastTimeStamp = startTimeStamp
        self.displayLink = self.createDisplayLink()
    }
    
    func cancel() {
        guard isRunning else { return }
        
        isRunning.toggle()
#if canImport(UIKit)
        displayLink?.invalidate()
#elseif canImport(AppKit)
        CVDisplayLinkStop(displayLink!)
#endif
        self.completionBlock?(false)
    }
    
    private func complete() {
        guard isRunning else { return }
        
        isRunning.toggle()
#if canImport(UIKit)
        displayLink?.invalidate()
#elseif canImport(AppKit)
        CVDisplayLinkStop(displayLink!)
#endif
        self.completionBlock?(true)
    }
    
    @objc private func displayLinkTick(_ displayLink: CADisplayLink) {
        guard isRunning else { return }
        
        let currentTimeStamp = CACurrentMediaTime()
        let progress = currentTimeStamp - startTimeStamp
        let elapsed = currentTimeStamp - lastTimeStamp
        lastTimeStamp = currentTimeStamp
        
        if progress < duration {
            animationBlock(progress, elapsed)
        } else {
            complete()
        }
    }
    
#if canImport(UIKit)
    private func createDisplayLink() -> CADisplayLink {
        let dl = CADisplayLink(target: self, selector: #selector(displayLinkTick(_:)))
        dl.add(to: .main, forMode: .common)
        
        return dl
    }
#elseif canImport(AppKit)
    private func createDisplayLink() -> CVDisplayLink? {
        var cvDisplayLink: CVDisplayLink?
        CVDisplayLinkCreateWithActiveCGDisplays(&cvDisplayLink)
        guard let displayLink = cvDisplayLink else { return nil }
        
        CVDisplayLinkSetOutputCallback(displayLink, { (displayLink, inNow, inOutputTime, flagsIn, flagsOut, userInfo) -> CVReturn in
            guard let context = userInfo else { return kCVReturnError }
            let synchedTimer = Unmanaged<SynchedTimer>.fromOpaque(context).takeUnretainedValue()
            synchedTimer.displayLinkTick()
            return kCVReturnSuccess
        }, Unmanaged.passUnretained(self).toOpaque())
        
        CVDisplayLinkStart(displayLink)
        return displayLink
    }
    
    @objc private func displayLinkTick() {
        guard isRunning else { return }
        
        let currentTimeStamp = CACurrentMediaTime()
        let progress = currentTimeStamp - startTimeStamp
        let elapsed = currentTimeStamp - lastTimeStamp
        lastTimeStamp = currentTimeStamp
        
        if progress < duration {
            animationBlock(progress, elapsed)
        } else {
            complete()
        }
    }
#endif
    
}

任何调试此问题的帮助将不胜感激。将其他原生 SwiftUI 手势添加到视图工作中(onTap、拖动等),但我希望在两个平台之间保持相似。 [1]:https://i.sstatic.net/Ch38BDrk.png

ios macos swiftui uikit appkit
1个回答
0
投票

经过一夜的调试,看来是

func gestureRecognizerShouldBegin(_ gestureRecognizer: NSGestureRecognizer) -> Bool {
            guard let pgr = gestureRecognizer as? NSPanGestureRecognizer else { return false }
            let velocity = pgr.velocity(in: view)
            print(abs(velocity.x) > abs(velocity.y), abs(velocity.x), abs(velocity.y))
            return abs(velocity.x) > abs(velocity.y)
        }

始终返回 false。因此需要进行更多调试,但至少它已经解决了!

© www.soinside.com 2019 - 2024. All rights reserved.