Skip to content

Commit 10bd1c5

Browse files
committed
New navigator content inset strategy
1 parent 8875768 commit 10bd1c5

File tree

8 files changed

+101
-36
lines changed

8 files changed

+101
-36
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,21 @@ All notable changes to this project will be documented in this file. Take a look
44

55
## [Unreleased]
66

7+
### Added
8+
9+
#### Navigator
10+
11+
* Added `VisualNavigatorDelegate.navigatorContentInset(_:)` to customize the content and safe-area insets used by the navigator.
12+
* By default, the navigator uses the window's `safeAreaInsets`, which can cause content to shift when the status bar is shown or hidden (since those insets change). To avoid this, implement `navigatorContentInset(_:)` and return insets that remain stable across status bar visibility changes — for example, a top inset large enough to accommodate the maximum expected status bar height.
13+
714
### Changed
815

16+
#### Navigator
17+
18+
* `EPUBNavigatorViewController.Configuration.contentInset` now expects values that already include the safe area insets.
19+
* If you previously supplied content-only margins, update them to add the safe-area values to preserve the same visible layout.
20+
* Alternatively, implement `VisualNavigatorDelegate.navigatorContentInset(_:)` to compute and return the full insets (content + safe area), helping avoid layout shifts when system UI (e.g., the status bar) appears or disappears.
21+
922
#### LCP
1023

1124
* The LCP License Document is now accessible via `publication.lcpLicense?.license`, even if the license validation fails with a status error or missing passphrase. This is useful for checking the end date of an expired license or renew a license.

Sources/Navigator/EPUB/EPUBFixedSpreadView.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,7 @@ final class EPUBFixedSpreadView: EPUBSpreadView {
7878
return
7979
}
8080

81-
// We use the window's safeAreaInsets instead of the view's because we
82-
// only want to take into account the device notch and status bar, not
83-
// the application's bars.
84-
var insets = window?.safeAreaInsets ?? .zero
81+
var insets = delegate?.spreadViewContentInset(self) ?? .zero
8582

8683
// Use the same insets on the left and right side (the largest one) to
8784
// keep the pages centered on the screen even if the notches are not

Sources/Navigator/EPUB/EPUBNavigatorViewController.swift

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,17 @@ open class EPUBNavigatorViewController: InputObservableViewController,
6565
/// Disables horizontal page turning when scroll is enabled.
6666
public var disablePageTurnsWhileScrolling: Bool
6767

68-
/// Content insets used to add some vertical margins around reflowable EPUB publications.
69-
/// The insets can be configured for each size class to allow smaller margins on compact
70-
/// screens.
68+
/// Content insets used to add some vertical margins around reflowable
69+
/// EPUB publications. Note that the margins include the safe area
70+
/// insets. To avoid any "jump" when toggling the status bar, provide
71+
/// values large enough.
72+
///
73+
/// The insets can be configured for each size class to allow smaller
74+
/// margins on compact screens.
75+
///
76+
/// For more control, implement the `navigatorContentInset()` delegate
77+
/// method, which takes precedence over this configuration property
78+
/// when implemented.
7179
public var contentInset: [UIUserInterfaceSizeClass: EPUBContentInsets]
7280

7381
/// Number of positions (as in `Publication.positionList`) to preload before the current page.
@@ -96,8 +104,8 @@ open class EPUBNavigatorViewController: InputObservableViewController,
96104
editingActions: [EditingAction] = EditingAction.defaultActions,
97105
disablePageTurnsWhileScrolling: Bool = false,
98106
contentInset: [UIUserInterfaceSizeClass: EPUBContentInsets] = [
99-
.compact: (top: 20, bottom: 20),
100-
.regular: (top: 44, bottom: 44),
107+
.compact: (top: 34, bottom: 34),
108+
.regular: (top: 62, bottom: 62),
101109
],
102110
preloadPreviousPositionCount: Int = 2,
103111
preloadNextPositionCount: Int = 6,
@@ -118,6 +126,13 @@ open class EPUBNavigatorViewController: InputObservableViewController,
118126
self.readiumCSSRSProperties = readiumCSSRSProperties
119127
self.debugState = debugState
120128
}
129+
130+
func contentInset(for sizeClass: UIUserInterfaceSizeClass) -> EPUBContentInsets {
131+
contentInset[sizeClass]
132+
?? contentInset[.regular]
133+
?? contentInset[.unspecified]
134+
?? (top: 0, bottom: 0)
135+
}
121136
}
122137

123138
public weak var delegate: EPUBNavigatorDelegate?
@@ -1006,6 +1021,25 @@ extension EPUBNavigatorViewController: EPUBNavigatorViewModelDelegate {
10061021
}
10071022

10081023
extension EPUBNavigatorViewController: EPUBSpreadViewDelegate {
1024+
func spreadViewContentInset(_ spreadView: EPUBSpreadView) -> UIEdgeInsets {
1025+
if let inset = delegate?.navigatorContentInset(self) {
1026+
return inset
1027+
}
1028+
1029+
// We use the window's safeAreaInsets instead of the view's because we
1030+
// only want to take into account the device notch and status bar, not
1031+
// the application's bars.
1032+
var insets = view.window?.safeAreaInsets ?? .zero
1033+
1034+
if publication.metadata.layout != .fixed {
1035+
let configInset = config.contentInset(for: view.traitCollection.verticalSizeClass)
1036+
insets.top = max(insets.top, configInset.top)
1037+
insets.bottom = max(insets.top, configInset.bottom)
1038+
}
1039+
1040+
return insets
1041+
}
1042+
10091043
func spreadViewDidLoad(_ spreadView: EPUBSpreadView) async {
10101044
let templates = config.decorationTemplates.reduce(into: [:]) { styles, item in
10111045
styles[item.key.rawValue] = item.value.json

Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -86,30 +86,16 @@ final class EPUBReflowableSpreadView: EPUBSpreadView {
8686
}
8787

8888
private func updateContentInset() {
89-
// We use the window's safeAreaInsets instead of the view's because we
90-
// only want to take into account the device notch and status bar, not
91-
// the application's bars.
92-
let safeAreaInsets = window?.safeAreaInsets ?? .zero
89+
let contentInset = delegate?.spreadViewContentInset(self) ?? .zero
9390

9491
if viewModel.scroll {
9592
topConstraint.constant = 0
9693
bottomConstraint.constant = 0
97-
scrollView.contentInset = UIEdgeInsets(top: safeAreaInsets.top, left: 0, bottom: safeAreaInsets.bottom, right: 0)
94+
scrollView.contentInset = contentInset
9895

9996
} else {
100-
let contentInset = viewModel.config.contentInset
101-
var insets = contentInset[traitCollection.verticalSizeClass]
102-
?? contentInset[.regular]
103-
?? contentInset[.unspecified]
104-
?? (top: 0, bottom: 0)
105-
106-
// Increases the insets by the window's safe area insets area to
107-
// make sure that the content is not overlapped by the screen notch.
108-
insets.top += safeAreaInsets.top
109-
insets.bottom += safeAreaInsets.bottom
110-
111-
topConstraint.constant = insets.top
112-
bottomConstraint.constant = -insets.bottom
97+
topConstraint.constant = contentInset.top
98+
bottomConstraint.constant = -contentInset.bottom
11399
scrollView.contentInset = .zero
114100
}
115101
}

Sources/Navigator/EPUB/EPUBSpreadView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import SwiftSoup
99
@preconcurrency import WebKit
1010

1111
protocol EPUBSpreadViewDelegate: AnyObject {
12+
/// Returns the content inset the spread view should use.
13+
func spreadViewContentInset(_ spreadView: EPUBSpreadView) -> UIEdgeInsets
14+
1215
/// Called when the spread view finished loading.
1316
func spreadViewDidLoad(_ spreadView: EPUBSpreadView) async
1417

Sources/Navigator/PDF/PDFDocumentView.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,21 @@
77
import Foundation
88
import PDFKit
99

10+
protocol PDFDocumentViewDelegate: AnyObject {
11+
func pdfDocumentViewContentInset(_ pdfDocumentView: PDFDocumentView) -> UIEdgeInsets?
12+
}
13+
1014
public final class PDFDocumentView: PDFView {
1115
var editingActions: EditingActionsController
16+
private weak var documentViewDelegate: PDFDocumentViewDelegate?
1217

13-
init(frame: CGRect, editingActions: EditingActionsController) {
18+
init(
19+
frame: CGRect,
20+
editingActions: EditingActionsController,
21+
documentViewDelegate: PDFDocumentViewDelegate
22+
) {
1423
self.editingActions = editingActions
24+
self.documentViewDelegate = documentViewDelegate
1525

1626
super.init(frame: frame)
1727

@@ -40,11 +50,7 @@ public final class PDFDocumentView: PDFView {
4050
}
4151

4252
private func updateContentInset() {
43-
// We use the window's safeAreaInsets instead of the view's because we
44-
// only want to take into account the device notch and status bar, not
45-
// the application's bars.
46-
let insets = window?.safeAreaInsets ?? .zero
47-
53+
let insets = documentViewDelegate?.pdfDocumentViewContentInset(self) ?? window?.safeAreaInsets ?? .zero
4854
firstScrollView?.contentInset.top = insets.top
4955
firstScrollView?.contentInset.bottom = insets.bottom
5056
}

Sources/Navigator/PDF/PDFNavigatorViewController.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,11 @@ open class PDFNavigatorViewController:
241241
}
242242

243243
currentResourceIndex = nil
244-
let pdfView = PDFDocumentView(frame: view.bounds, editingActions: editingActions)
244+
let pdfView = PDFDocumentView(
245+
frame: view.bounds,
246+
editingActions: editingActions,
247+
documentViewDelegate: self
248+
)
245249
self.pdfView = pdfView
246250
pdfView.delegate = self
247251
pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
@@ -659,6 +663,12 @@ extension PDFNavigatorViewController: PDFViewDelegate {
659663
}
660664
}
661665

666+
extension PDFNavigatorViewController: PDFDocumentViewDelegate {
667+
func pdfDocumentViewContentInset(_ pdfDocumentView: PDFDocumentView) -> UIEdgeInsets? {
668+
delegate?.navigatorContentInset(self)
669+
}
670+
}
671+
662672
extension PDFNavigatorViewController: EditingActionsControllerDelegate {
663673
func editingActionsDidPreventCopy(_ editingActions: EditingActionsController) {
664674
delegate?.navigator(self, presentError: .copyForbidden)

Sources/Navigator/VisualNavigator.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,21 @@ public struct VisualNavigatorPresentation {
8686
}
8787

8888
@MainActor public protocol VisualNavigatorDelegate: NavigatorDelegate {
89+
/// Returns the content insets that the navigator applies to its view.
90+
///
91+
/// Implement this method to customize the margins around the publication
92+
/// content and to control which areas may be covered by the app's UI or
93+
/// system bars.
94+
///
95+
/// Consider the view's safe area insets to prevent notches, the status bar,
96+
/// or other overlays from obscuring the content.
97+
///
98+
/// - Returns: The insets to apply, or `nil` to use the navigator’s default behavior.
99+
func navigatorContentInset(_ navigator: VisualNavigator) -> UIEdgeInsets?
100+
89101
/// Called when the navigator presentation changed, for example after
90102
/// applying a new set of preferences.
91-
func navigator(_ navigator: Navigator, presentationDidChange presentation: VisualNavigatorPresentation)
103+
func navigator(_ navigator: VisualNavigator, presentationDidChange presentation: VisualNavigatorPresentation)
92104

93105
/// Called when the user tapped the publication, and it didn't trigger any
94106
/// internal action. The point is relative to the navigator's view.
@@ -110,7 +122,11 @@ public struct VisualNavigatorPresentation {
110122
}
111123

112124
public extension VisualNavigatorDelegate {
113-
func navigator(_ navigator: Navigator, presentationDidChange presentation: VisualNavigatorPresentation) {
125+
func navigatorContentInset(_ navigator: VisualNavigator) -> UIEdgeInsets? {
126+
nil
127+
}
128+
129+
func navigator(_ navigator: VisualNavigator, presentationDidChange presentation: VisualNavigatorPresentation) {
114130
// Optional
115131
}
116132

0 commit comments

Comments
 (0)