[![введите описание изображения здесь][1]][1]
В настоящее время код отлично работает на iOS, и я могу перетаскивать ползунок влево и вправо для изменения значений. В macOS (AppKit), хотя пользовательский интерфейс выглядит хорошо, перетаскивание ползунка, похоже, не работает.
Вот 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, 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.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
}
[1]: https://i.sstatic.net/Ch38BDrk.png
Подробнее здесь: https://stackoverflow.com/questions/785 ... tiplatform