我发现了一个旧的 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
经过一夜的调试,看来是
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。因此需要进行更多调试,但至少它已经解决了!