From 25e36a8c16e5c718c6ba90b4dc3fea3113270414 Mon Sep 17 00:00:00 2001 From: Leonard Beus Date: Wed, 21 Jan 2026 10:03:32 +0100 Subject: [PATCH 1/2] Add did scroll event callback to VisualNavigatorDelegate --- CHANGELOG.md | 1 + .../EPUB/EPUBNavigatorViewController.swift | 6 ++++++ .../EPUB/EPUBReflowableSpreadView.swift | 17 +++++++++++++++++ Sources/Navigator/EPUB/EPUBSpreadView.swift | 3 +++ Sources/Navigator/VisualNavigator.swift | 18 ++++++++++++++++++ 5 files changed, 45 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c2dc68b3..ac34bd3ad8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. Take a look * Support for displaying Divina (image-based publications like CBZ) in the fixed-layout EPUB navigator. * Bitmap images in the EPUB reading order are now supported as a fixed layout resource. * Added `offsetFirstPage` preference for fixed-layout EPUBs to control whether the first page is displayed alone or alongside the second page when spreads are enabled. +* Added `func navigator(_ navigator: VisualNavigator, didScrollIn direction: ScrollDirection)` to `VisualNavigatorDelegate` protocol #### Streamer diff --git a/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift b/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift index a88c2e8908..92476a2658 100644 --- a/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift +++ b/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift @@ -1250,6 +1250,12 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { } } + func spreadView(_ spreadView: EPUBSpreadView, didScrollIn direction: ScrollDirection) { + if paginationView?.currentView == spreadView { + delegate?.navigator(self, didScrollIn: direction) + } + } + func spreadView(_ spreadView: EPUBSpreadView, present viewController: UIViewController) { present(viewController, animated: true) } diff --git a/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift b/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift index df308ca1fe..6c7ef84f07 100644 --- a/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift +++ b/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift @@ -317,6 +317,8 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { private var progression: ClosedRange? // To check if a progression change was cancelled or not. private var previousProgression: ClosedRange? + // Previous scroll offset for determining scroll direction. + private var previousScrollOffset: CGPoint? // Called by the javascript code to notify that scrolling ended. private func progressionDidChange(_ body: Any) { @@ -380,5 +382,20 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { override func scrollViewDidScroll(_ scrollView: UIScrollView) { super.scrollViewDidScroll(scrollView) setNeedsNotifyPagesDidChange() + + let currentOffset = scrollView.contentOffset + if let previousOffset = previousScrollOffset { + // Determine direction based on scroll axis (vertical vs horizontal) + let direction: ScrollDirection + if viewModel.scroll, !viewModel.verticalText { + // Vertical scrolling mode + direction = currentOffset.y > previousOffset.y ? .forward : .backward + } else { + // Horizontal scrolling/pagination mode + direction = currentOffset.x > previousOffset.x ? .forward : .backward + } + delegate?.spreadView(self, didScrollIn: direction) + } + previousScrollOffset = currentOffset } } diff --git a/Sources/Navigator/EPUB/EPUBSpreadView.swift b/Sources/Navigator/EPUB/EPUBSpreadView.swift index 414fdaf88c..e1f84906f3 100644 --- a/Sources/Navigator/EPUB/EPUBSpreadView.swift +++ b/Sources/Navigator/EPUB/EPUBSpreadView.swift @@ -30,6 +30,9 @@ protocol EPUBSpreadViewDelegate: AnyObject { /// Called when the pages visible in the spread changed. func spreadViewPagesDidChange(_ spreadView: EPUBSpreadView) + /// Called when the user scrolls through the content. + func spreadView(_ spreadView: EPUBSpreadView, didScrollIn direction: ScrollDirection) + /// Called when the spread view needs to present a view controller. func spreadView(_ spreadView: EPUBSpreadView, present viewController: UIViewController) diff --git a/Sources/Navigator/VisualNavigator.swift b/Sources/Navigator/VisualNavigator.swift index 6c9a508e5c..b6f6cee347 100644 --- a/Sources/Navigator/VisualNavigator.swift +++ b/Sources/Navigator/VisualNavigator.swift @@ -85,6 +85,14 @@ public struct VisualNavigatorPresentation { } } +/// Direction of a scroll event. +public enum ScrollDirection { + /// Scrolling up (vertical) or left (horizontal). + case backward + /// Scrolling down (vertical) or right (horizontal). + case forward +} + @MainActor public protocol VisualNavigatorDelegate: NavigatorDelegate { /// Returns the content insets that the navigator applies to its view. /// @@ -119,6 +127,12 @@ public struct VisualNavigatorPresentation { /// Return `true` to navigate to the link, or `false` if you intend to /// present the link yourself func navigator(_ navigator: VisualNavigator, shouldNavigateToLink link: Link) -> Bool + + /// Called when the user scrolls through the content. + /// + /// - Parameter direction: The direction of the scroll (forward = down/right, + /// backward = up/left). + func navigator(_ navigator: VisualNavigator, didScrollIn direction: ScrollDirection) } public extension VisualNavigatorDelegate { @@ -145,4 +159,8 @@ public extension VisualNavigatorDelegate { func navigator(_ navigator: VisualNavigator, shouldNavigateToLink link: Link) -> Bool { true } + + func navigator(_ navigator: VisualNavigator, didScrollIn direction: ScrollDirection) { + // Optional + } } From 7430e8c29b05a54fc62e861d0b7f3f0c0cc1a2b5 Mon Sep 17 00:00:00 2001 From: Leonard Beus Date: Mon, 18 May 2026 10:45:30 +0200 Subject: [PATCH 2/2] Extend VisualNavigator with scroll drag events --- CHANGELOG.md | 2 +- .../EPUB/EPUBNavigatorViewController.swift | 10 +++- .../EPUB/EPUBReflowableSpreadView.swift | 51 ++++++++++++++----- Sources/Navigator/EPUB/EPUBSpreadView.swift | 7 ++- Sources/Navigator/VisualNavigator.swift | 31 ++++++----- 5 files changed, 70 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b571e53903..4b6f9c035a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -135,7 +135,7 @@ All notable changes to this project will be documented in this file. Take a look * Support for displaying Divina (image-based publications like CBZ) in the fixed-layout EPUB navigator. * Bitmap images in the EPUB reading order are now supported as a fixed layout resource. * Added `offsetFirstPage` preference for fixed-layout EPUBs to control whether the first page is displayed alone or alongside the second page when spreads are enabled. -* Added `func navigator(_ navigator: VisualNavigator, didScrollIn direction: ScrollDirection)` to `VisualNavigatorDelegate` protocol +* Added `func navigator(_ navigator: any VisualNavigator, didDragBy delta: CGFloat)` and `func navigator(_ navigator: any VisualNavigator, didEndDraggingWithVelocity velocity: CGFloat)` to `VisualNavigatorDelegate`. #### Streamer diff --git a/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift b/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift index fc493b1434..445558ee74 100644 --- a/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift +++ b/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift @@ -1232,9 +1232,15 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { } } - func spreadView(_ spreadView: EPUBSpreadView, didScrollIn direction: ScrollDirection) { + func spreadView(_ spreadView: EPUBSpreadView, didDragBy delta: CGFloat) { if paginationView?.currentView == spreadView { - delegate?.navigator(self, didScrollIn: direction) + delegate?.navigator(self, didDragBy: delta) + } + } + + func spreadView(_ spreadView: EPUBSpreadView, didEndDraggingWithVelocity velocity: CGFloat) { + if paginationView?.currentView == spreadView { + delegate?.navigator(self, didEndDraggingWithVelocity: velocity) } } diff --git a/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift b/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift index 6c25ccd8fd..4f3911e2e8 100644 --- a/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift +++ b/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift @@ -370,8 +370,8 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { private var progression: ClosedRange? /// To check if a progression change was cancelled or not. private var previousProgression: ClosedRange? - // Previous scroll offset for determining scroll direction. - private var previousScrollOffset: CGPoint? + /// Previous scroll offset while the user is dragging. + private var previousDragOffset: CGPoint? /// Called by the javascript code to notify that scrolling ended. private func progressionDidChange(_ body: Any) { @@ -434,23 +434,48 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { // MARK: - UIScrollViewDelegate + override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + super.scrollViewWillBeginDragging(scrollView) + previousDragOffset = scrollView.contentOffset + } + override func scrollViewDidScroll(_ scrollView: UIScrollView) { super.scrollViewDidScroll(scrollView) setNeedsNotifyPagesDidChange() let currentOffset = scrollView.contentOffset - if let previousOffset = previousScrollOffset { - // Determine direction based on scroll axis (vertical vs horizontal) - let direction: ScrollDirection - if viewModel.scroll, !viewModel.verticalText { - // Vertical scrolling mode - direction = currentOffset.y > previousOffset.y ? .forward : .backward - } else { - // Horizontal scrolling/pagination mode - direction = currentOffset.x > previousOffset.x ? .forward : .backward + if scrollView.isDragging, let previousOffset = previousDragOffset { + let delta = progressionDelta(from: previousOffset, to: currentOffset) + if delta != 0 { + delegate?.spreadView(self, didDragBy: delta) } - delegate?.spreadView(self, didScrollIn: direction) } - previousScrollOffset = currentOffset + previousDragOffset = currentOffset + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + delegate?.spreadView(self, didEndDraggingWithVelocity: progressionVelocity(from: velocity)) + previousDragOffset = nil + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + previousDragOffset = nil + } + + private func progressionDelta(from previousOffset: CGPoint, to currentOffset: CGPoint) -> CGFloat { + if viewModel.scroll, !viewModel.verticalText { + return currentOffset.y - previousOffset.y + } + + let delta = currentOffset.x - previousOffset.x + return viewModel.readingProgression == .rtl ? -delta : delta + } + + private func progressionVelocity(from velocity: CGPoint) -> CGFloat { + if viewModel.scroll, !viewModel.verticalText { + return velocity.y + } + + return viewModel.readingProgression == .rtl ? -velocity.x : velocity.x } } diff --git a/Sources/Navigator/EPUB/EPUBSpreadView.swift b/Sources/Navigator/EPUB/EPUBSpreadView.swift index cbc4435ec9..7fb1fb44e1 100644 --- a/Sources/Navigator/EPUB/EPUBSpreadView.swift +++ b/Sources/Navigator/EPUB/EPUBSpreadView.swift @@ -29,8 +29,11 @@ protocol EPUBSpreadViewDelegate: AnyObject { /// Called when the pages visible in the spread changed. func spreadViewPagesDidChange(_ spreadView: EPUBSpreadView) - /// Called when the user scrolls through the content. - func spreadView(_ spreadView: EPUBSpreadView, didScrollIn direction: ScrollDirection) + /// Called continuously while the user drags the content. + func spreadView(_ spreadView: EPUBSpreadView, didDragBy delta: CGFloat) + + /// Called when the user lifts their finger after dragging. + func spreadView(_ spreadView: EPUBSpreadView, didEndDraggingWithVelocity velocity: CGFloat) /// Called when the spread view needs to present a view controller. func spreadView(_ spreadView: EPUBSpreadView, present viewController: UIViewController) diff --git a/Sources/Navigator/VisualNavigator.swift b/Sources/Navigator/VisualNavigator.swift index d1878c18e9..1da90d5903 100644 --- a/Sources/Navigator/VisualNavigator.swift +++ b/Sources/Navigator/VisualNavigator.swift @@ -83,14 +83,6 @@ public struct VisualNavigatorPresentation { } } -/// Direction of a scroll event. -public enum ScrollDirection { - /// Scrolling up (vertical) or left (horizontal). - case backward - /// Scrolling down (vertical) or right (horizontal). - case forward -} - @MainActor public protocol VisualNavigatorDelegate: NavigatorDelegate { /// Returns the content insets that the navigator applies to its view. /// @@ -126,11 +118,20 @@ public enum ScrollDirection { /// present the link yourself func navigator(_ navigator: VisualNavigator, shouldNavigateToLink link: Link) -> Bool - /// Called when the user scrolls through the content. + /// Called continuously while the user drags the content. + /// + /// `delta` is the distance in points along the scroll axis since the last + /// call. Positive values indicate forward progression through the content. + /// + /// Not called during momentum deceleration after the user lifts their + /// finger. + func navigator(_ navigator: any VisualNavigator, didDragBy delta: CGFloat) + + /// Called when the user lifts their finger after dragging. /// - /// - Parameter direction: The direction of the scroll (forward = down/right, - /// backward = up/left). - func navigator(_ navigator: VisualNavigator, didScrollIn direction: ScrollDirection) + /// `velocity` is in points per second along the scroll axis. Positive + /// values indicate forward momentum. + func navigator(_ navigator: any VisualNavigator, didEndDraggingWithVelocity velocity: CGFloat) } public extension VisualNavigatorDelegate { @@ -158,7 +159,11 @@ public extension VisualNavigatorDelegate { true } - func navigator(_ navigator: VisualNavigator, didScrollIn direction: ScrollDirection) { + func navigator(_ navigator: any VisualNavigator, didDragBy delta: CGFloat) { + // Optional + } + + func navigator(_ navigator: any VisualNavigator, didEndDraggingWithVelocity velocity: CGFloat) { // Optional } }