From 705b7b00a43cd438c6bb2732e97fcee777af3094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Wed, 16 Sep 2026 12:36:16 +0200 Subject: [PATCH 1/5] Fix launching the Test App on iOS 27 --- TestApp/Sources/AppDelegate.swift | 68 ++++------------------ TestApp/Sources/Info.plist | 65 +++++++++++++-------- TestApp/Sources/SceneDelegate.swift | 89 +++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 81 deletions(-) create mode 100644 TestApp/Sources/SceneDelegate.swift diff --git a/TestApp/Sources/AppDelegate.swift b/TestApp/Sources/AppDelegate.swift index 03ac3459d2..93166a9da1 100644 --- a/TestApp/Sources/AppDelegate.swift +++ b/TestApp/Sources/AppDelegate.swift @@ -4,71 +4,25 @@ // available in the top-level LICENSE file of the project. // -import Combine -import ReadiumShared import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - private var app: AppModule! - private var subscriptions = Set() + private(set) static var app: AppModule! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - app = try! AppModule() - - func makeItem(title: String, image: String) -> UITabBarItem { - UITabBarItem( - title: NSLocalizedString(title, comment: "Library tab title"), - image: UIImage(named: image), - tag: 0 - ) - } - - // Library - let libraryViewController = app.library.rootViewController - libraryViewController.tabBarItem = makeItem(title: "bookshelf_tab", image: "bookshelf") - - // OPDS Feeds - let opdsViewController = app.opds.rootViewController - opdsViewController.tabBarItem = makeItem(title: "catalogs_tab", image: "catalogs") - - // About - let aboutViewController = app.aboutViewController - aboutViewController.tabBarItem = makeItem(title: "about_tab", image: "about") - - let tabBarController = UITabBarController() - tabBarController.viewControllers = [ - libraryViewController, - opdsViewController, - aboutViewController, - ] - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = tabBarController - window?.makeKeyAndVisible() - + Self.app = try! AppModule() return true } - func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - guard let url = url.anyURL.absoluteURL, let vc = window?.rootViewController else { - return false - } - - Task { - do { - try await app.library.importPublication(from: url, progress: { _ in }) - } catch { - guard let error = error as? UserErrorConvertible else { - print(error) - return - } - vc.alert(error) - } - } - - return true + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + UISceneConfiguration( + name: "Default Configuration", + sessionRole: connectingSceneSession.role + ) } } diff --git a/TestApp/Sources/Info.plist b/TestApp/Sources/Info.plist index 1f0835111d..66b0535729 100644 --- a/TestApp/Sources/Info.plist +++ b/TestApp/Sources/Info.plist @@ -216,30 +216,30 @@ org.readium.lcpa - - CFBundleTypeName - Zipped Audiobook - CFBundleTypeRole - Viewer - LSHandlerRank - Alternate - LSItemContentTypes - - org.readium.zab - - - - CFBundleTypeName - Audiobook - CFBundleTypeRole - Viewer - LSHandlerRank - Alternate - LSItemContentTypes - - public.audio - - + + CFBundleTypeName + Zipped Audiobook + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + org.readium.zab + + + + CFBundleTypeName + Audiobook + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + public.audio + + CFBundleExecutable $(EXECUTABLE_NAME) @@ -264,6 +264,23 @@ NSAllowsArbitraryLoads + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + UIBackgroundModes audio diff --git a/TestApp/Sources/SceneDelegate.swift b/TestApp/Sources/SceneDelegate.swift new file mode 100644 index 0000000000..aa5db30f83 --- /dev/null +++ b/TestApp/Sources/SceneDelegate.swift @@ -0,0 +1,89 @@ +// +// Copyright 2026 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. +// + +import ReadiumShared +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + private var app: AppModule { + AppDelegate.app + } + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = scene as? UIWindowScene else { + return + } + + func makeItem(title: String, image: String) -> UITabBarItem { + UITabBarItem( + title: NSLocalizedString(title, comment: "Library tab title"), + image: UIImage(named: image), + tag: 0 + ) + } + + // Library + let libraryViewController = app.library.rootViewController + libraryViewController.tabBarItem = makeItem(title: "bookshelf_tab", image: "bookshelf") + + // OPDS Feeds + let opdsViewController = app.opds.rootViewController + opdsViewController.tabBarItem = makeItem(title: "catalogs_tab", image: "catalogs") + + // About + let aboutViewController = app.aboutViewController + aboutViewController.tabBarItem = makeItem(title: "about_tab", image: "about") + + let tabBarController = UITabBarController() + tabBarController.viewControllers = [ + libraryViewController, + opdsViewController, + aboutViewController, + ] + + let window = UIWindow(windowScene: windowScene) + window.rootViewController = tabBarController + window.makeKeyAndVisible() + self.window = window + + // The scene can be launched by opening a publication, in which case + // the URLs are delivered here instead of `scene(_:openURLContexts:)`. + importPublications(from: connectionOptions.urlContexts) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + importPublications(from: URLContexts) + } + + private func importPublications(from contexts: Set) { + for context in contexts { + guard let url = context.url.anyURL.absoluteURL else { + continue + } + + Task { + do { + try await app.library.importPublication(from: url, progress: { _ in }) + } catch { + guard + let error = error as? UserErrorConvertible, + let vc = window?.rootViewController + else { + print(error) + return + } + vc.alert(error) + } + } + } + } +} From 4598af9b080b4237f9d369bcd0d19254ef8c55da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Wed, 16 Sep 2026 12:46:29 +0200 Subject: [PATCH 2/5] Fix PDF content being inset by the navigation bar on iOS 27 Since iOS 27, PDFKit lays out pages inside the PDFView's bounds inset by its safeAreaInsets instead of the full bounds. In paginated mode this bypasses the scroll view's contentInset entirely, so neither contentInsetAdjustmentBehavior = .never nor zeroing contentInset prevented the content from shifting when the navigation bar was shown. PDFDocumentView now overrides safeAreaInsets to report the insets it already computes, so PDFKit lays the content out where the navigator asks. This also makes the pdfDocumentViewContentInset delegate hook effective in paginated mode on iOS 27, where it was previously ignored. This behavior is undocumented. It was identified by measuring where PDFKit places a page in a paginated PDFView inside a navigation controller, across three runtimes: with a 390x844 view, a 612x792 page and the nav bar shown, the page lands centered in the full bounds on iOS 18.6 (y=172.9) and 26.4 (y=172.9), but centered in the safe area on iOS 27.0 (y=204.9). The measurement is recorded in the code comment. --- Sources/Navigator/PDF/PDFDocumentView.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/Navigator/PDF/PDFDocumentView.swift b/Sources/Navigator/PDF/PDFDocumentView.swift index 1661184e6c..e02a9507ea 100644 --- a/Sources/Navigator/PDF/PDFDocumentView.swift +++ b/Sources/Navigator/PDF/PDFDocumentView.swift @@ -57,6 +57,18 @@ public final class PDFDocumentView: PDFView { documentViewDelegate?.pdfDocumentView(self, didGoTo: destination) } + /// Since iOS 27, PDFKit lays out the pages inside the view's bounds inset + /// by its `safeAreaInsets`, instead of the full bounds. In paginated mode + /// this is not routed through the scroll view's `contentInset` at all, so + /// neither `contentInsetAdjustmentBehavior` nor zeroing `contentInset` has + /// any effect on it. + /// + /// We report our own insets instead, so that PDFKit lays out the content + /// exactly where we want it. + override public var safeAreaInsets: UIEdgeInsets { + contentInset + } + override public func safeAreaInsetsDidChange() { super.safeAreaInsetsDidChange() updateContentInset() From 16fdb3dadd3574d11168e5038c6ee792c76770a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Wed, 16 Sep 2026 14:07:17 +0200 Subject: [PATCH 3/5] Fix presentation of the reader --- .../Sources/Reader/Common/Preferences/UserPreferences.swift | 4 ++-- TestApp/Sources/Reader/Common/ReaderViewController.swift | 5 +++++ TestApp/Sources/Reader/ReaderModule.swift | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/TestApp/Sources/Reader/Common/Preferences/UserPreferences.swift b/TestApp/Sources/Reader/Common/Preferences/UserPreferences.swift index 2d78d080b2..4af7ec6f1b 100644 --- a/TestApp/Sources/Reader/Common/Preferences/UserPreferences.swift +++ b/TestApp/Sources/Reader/Common/Preferences/UserPreferences.swift @@ -760,7 +760,7 @@ struct UserPreferences< isActive: isActive, onClear: onClear ) { - HStack { + HStack(spacing: 4) { Stepper(title, onIncrement: onIncrement, onDecrement: onDecrement) @@ -835,7 +835,7 @@ struct UserPreferences< onClear: @escaping () -> Void, content: @escaping () -> V ) -> some View { - HStack { + HStack(spacing: 8) { content() .foregroundColor(isActive ? nil : .gray) diff --git a/TestApp/Sources/Reader/Common/ReaderViewController.swift b/TestApp/Sources/Reader/Common/ReaderViewController.swift index e6213af835..7d5f322f80 100644 --- a/TestApp/Sources/Reader/Common/ReaderViewController.swift +++ b/TestApp/Sources/Reader/Common/ReaderViewController.swift @@ -56,9 +56,14 @@ class ReaderViewController: UIViewController, override func viewDidLoad() { super.viewDidLoad() + navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .plain, target: self, action: #selector(close)) navigationItem.rightBarButtonItems = makeNavigationBarButtons() } + @objc private func close() { + dismiss(animated: true) + } + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) diff --git a/TestApp/Sources/Reader/ReaderModule.swift b/TestApp/Sources/Reader/ReaderModule.swift index 078cf44801..b68892d4b9 100644 --- a/TestApp/Sources/Reader/ReaderModule.swift +++ b/TestApp/Sources/Reader/ReaderModule.swift @@ -62,9 +62,9 @@ final class ReaderModule: ReaderModuleAPI { @MainActor func present(_ viewController: UIViewController) { let backItem = UIBarButtonItem() backItem.title = "" - viewController.navigationItem.backBarButtonItem = backItem - viewController.hidesBottomBarWhenPushed = true - navigationController.pushViewController(viewController, animated: true) + let host = UINavigationController(rootViewController: viewController) + host.modalPresentationStyle = .fullScreen + navigationController.present(host, animated: true) } guard let module = self.formatModules.first(where: { $0.supports(publication) }) else { From f8cbc893be971c992879aa9d1baec2f3a4606d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Wed, 16 Sep 2026 14:23:29 +0200 Subject: [PATCH 4/5] Update the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d503ee4988..4a7b117be3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ All notable changes to this project will be documented in this file. Take a look #### Navigator * [#121](https://github.com/readium/swift-toolkit/issues/121) HTML `