ReadViewSDK/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextContentInteractionCoordinator.swift

249 lines
9.1 KiB
Swift

import UIKit
final class RDEPUBTextContentInteractionCoordinator: NSObject {
enum SelectionInteractionState: Equatable {
case idle
case selectionPending
case selecting
case selectionActive
case adjustingHandle
}
struct Dependencies {
let hasRenderableContent: () -> Bool
let currentSelectionProvider: () -> RDEPUBSelection?
let isSelectionControllerSelecting: () -> Bool
let hasActiveSelection: () -> Bool
let selectionHandleAtPoint: (CGPoint) -> RDEPUBTextSelectionController.BoundaryHandle?
let selectionContainsPoint: (CGPoint) -> Bool
let renderPointForGesture: (UIGestureRecognizer) -> CGPoint
let renderPointForTouch: (UITouch) -> CGPoint
let performLongPressSelection: (UILongPressGestureRecognizer) -> Void
let performPanSelection: (UIPanGestureRecognizer) -> Void
let adjustSelection: (RDEPUBTextSelectionController.BoundaryHandle, CGPoint) -> Void
let presentLoupeAtPoint: (CGPoint) -> Void
let dismissLoupe: () -> Void
let showSelectionMenu: () -> Void
let hideSelectionMenu: () -> Void
let selectionTapSuppressionDidChange: (Bool) -> Void
let selectionPagingSuppressionDidChange: (Bool) -> Void
}
private let dependencies: Dependencies
private var activeSelectionHandle: RDEPUBTextSelectionController.BoundaryHandle?
private(set) var interactionState: SelectionInteractionState = .idle
var isInteractionInProgress: Bool {
interactionState != .idle
}
init(dependencies: Dependencies) {
self.dependencies = dependencies
super.init()
}
func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
dependencies.performLongPressSelection(gesture)
switch gesture.state {
case .began:
activeSelectionHandle = nil
updateSelectionInteractionState(.selecting)
dependencies.hideSelectionMenu()
dependencies.presentLoupeAtPoint(dependencies.renderPointForGesture(gesture))
case .changed:
updateSelectionInteractionState(.selecting)
dependencies.presentLoupeAtPoint(dependencies.renderPointForGesture(gesture))
case .ended:
activeSelectionHandle = nil
dependencies.dismissLoupe()
dependencies.showSelectionMenu()
case .cancelled, .failed:
activeSelectionHandle = nil
dependencies.dismissLoupe()
default:
break
}
}
func handlePan(_ gesture: UIPanGestureRecognizer) {
let point = dependencies.renderPointForGesture(gesture)
if gesture.state == .began, activeSelectionHandle == nil,
let handle = dependencies.selectionHandleAtPoint(point) {
activeSelectionHandle = handle
updateSelectionInteractionState(.adjustingHandle)
dependencies.hideSelectionMenu()
dependencies.presentLoupeAtPoint(point)
}
if let activeSelectionHandle {
dependencies.adjustSelection(activeSelectionHandle, point)
dependencies.presentLoupeAtPoint(point)
} else {
dependencies.performPanSelection(gesture)
if dependencies.isSelectionControllerSelecting() {
dependencies.presentLoupeAtPoint(point)
}
}
switch gesture.state {
case .ended:
activeSelectionHandle = nil
updateSelectionInteractionState(
dependencies.currentSelectionProvider() == nil ? .idle : .selectionActive
)
dependencies.dismissLoupe()
dependencies.showSelectionMenu()
case .cancelled, .failed:
activeSelectionHandle = nil
updateSelectionInteractionState(
dependencies.currentSelectionProvider() == nil ? .idle : .selectionActive
)
dependencies.dismissLoupe()
default:
break
}
}
func selectionControllerStateDidChange(_ state: RDEPUBTextSelectionController.InteractionState) {
switch state {
case .idle:
if dependencies.currentSelectionProvider() == nil, activeSelectionHandle == nil {
updateSelectionInteractionState(.idle)
}
case .selecting:
updateSelectionInteractionState(.selecting)
case .selectionActive:
updateSelectionInteractionState(
dependencies.currentSelectionProvider() == nil ? .idle : .selectionActive
)
case .adjustingHandle:
updateSelectionInteractionState(.adjustingHandle)
}
}
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard interactionState == .idle,
activeSelectionHandle == nil,
dependencies.hasRenderableContent(),
let touch = touches.first else {
return
}
let point = dependencies.renderPointForTouch(touch)
guard dependencies.selectionHandleAtPoint(point) == nil else { return }
updateSelectionInteractionState(.selectionPending)
}
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
resetSelectionPendingIfNeeded()
}
func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
resetSelectionPendingIfNeeded()
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer {
if dependencies.isSelectionControllerSelecting() {
return true
}
let point = dependencies.renderPointForGesture(gestureRecognizer)
return dependencies.selectionHandleAtPoint(point) != nil
}
if gestureRecognizer is UILongPressGestureRecognizer {
guard dependencies.hasRenderableContent() else {
return true
}
let point = dependencies.renderPointForGesture(gestureRecognizer)
if dependencies.selectionHandleAtPoint(point) != nil {
return false
}
if dependencies.hasActiveSelection(), dependencies.selectionContainsPoint(point) {
return false
}
return true
}
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
guard dependencies.hasRenderableContent() else {
return true
}
let point = dependencies.renderPointForTouch(touch)
if let handle = dependencies.selectionHandleAtPoint(point) {
if gestureRecognizer is UIPanGestureRecognizer {
activeSelectionHandle = handle
updateSelectionInteractionState(.adjustingHandle)
dependencies.hideSelectionMenu()
return true
}
if gestureRecognizer is UILongPressGestureRecognizer || gestureRecognizer is UITapGestureRecognizer {
return false
}
}
return true
}
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
gestureRecognizer is UILongPressGestureRecognizer || gestureRecognizer is UIPanGestureRecognizer
}
func reset() {
activeSelectionHandle = nil
updateSelectionInteractionState(.idle)
dependencies.dismissLoupe()
}
private func resetSelectionPendingIfNeeded() {
guard interactionState == .selectionPending else { return }
if dependencies.currentSelectionProvider() != nil {
updateSelectionInteractionState(.selectionActive)
} else {
updateSelectionInteractionState(.idle)
}
}
private func updateSelectionInteractionState(_ state: SelectionInteractionState) {
let previousTapSuppressed = interactionState != .idle
let previousPagingSuppressed = shouldSuppressPagingInteraction(for: interactionState)
let previousState = interactionState
interactionState = state
let currentTapSuppressed = interactionState != .idle
let currentPagingSuppressed = shouldSuppressPagingInteraction(for: interactionState)
if previousState != state {
RDReaderTapDebug.log(
"TextContentInteraction.state",
"transition \(previousState) -> \(state) tapSuppressed=\(currentTapSuppressed) pagingSuppressed=\(currentPagingSuppressed)"
)
}
if previousTapSuppressed != currentTapSuppressed {
dependencies.selectionTapSuppressionDidChange(currentTapSuppressed)
}
if previousPagingSuppressed != currentPagingSuppressed {
dependencies.selectionPagingSuppressionDidChange(currentPagingSuppressed)
}
}
private func shouldSuppressPagingInteraction(for state: SelectionInteractionState) -> Bool {
switch state {
case .idle, .selectionPending, .selectionActive:
return false
case .selecting, .adjustingHandle:
return true
}
}
}