NSPanGesture не работает на AppKit, но UIPanGesture работает на iOS (мультиплатформенное приложение)IOS

Программируем под IOS
Anonymous
NSPanGesture не работает на AppKit, но UIPanGesture работает на iOS (мультиплатформенное приложение)

Сообщение Anonymous »

Я нашел старую библиотеку SwiftUI, которую пытался портировать на MacOS. Полный репозиторий можно найти здесь: https://github.com/cyrilzakka/SlidingRuler. По сути, это скользящая линейка с привязкой тикера. Скриншот доступен ниже:
[![введите описание изображения здесь][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

}
Будем очень признательны за любую помощь в отладке. Добавление других встроенных жестов SwiftUI в работу с представлением (onTap, перетаскивание и т. д.), но я хочу, чтобы обе платформы были одинаковыми.
[1]: https://i.sstatic.net/Ch38BDrk.png

Подробнее здесь: https://stackoverflow.com/questions/785 ... tiplatform

Вернуться в «IOS»