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
Реклама
Ответить Пред. темаСлед. тема

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

  • Похожие темы
    Ответы
    Просмотры
    Последнее сообщение

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