Show action buttons in export previews

QLPreviewController only renders its share, print and open-in actions
when it is inside a navigation controller, so diagram and report
previews were dead ends. Wrap it in one, add an explicit Done button
and assert both in a UI test.
This commit is contained in:
2026-08-12 11:48:39 +02:00
parent a714a0d0d5
commit 695d2ccd75
3 changed files with 76 additions and 11 deletions

View File

@@ -232,8 +232,10 @@ struct LoadsView: View {
)
) {
if let previewURL {
QuickLookPreview(url: previewURL)
.ignoresSafeArea()
QuickLookPreview(url: previewURL) {
self.previewURL = nil
}
.ignoresSafeArea()
}
}
.onChange(of: previewURL) { _, newValue in

View File

@@ -5,38 +5,53 @@
import QuickLook
import SwiftUI
import UIKit
/// Presents a single exported file (PDF or PNG) in Quick Look so the user can
/// inspect it before sharing.
///
/// SwiftUI's `quickLookPreview(_:)` modifier is macOS-only, so iOS wraps
/// `QLPreviewController` directly.
/// `QLPreviewController` directly. The controller only shows its action
/// buttons — share, print, open in another app — when it sits inside a
/// navigation controller, so it is wrapped in one and gets an explicit Done
/// button to close the sheet.
struct QuickLookPreview: UIViewControllerRepresentable {
let url: URL
var onDone: () -> Void
func makeUIViewController(context: Context) -> QLPreviewController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
return controller
func makeUIViewController(context: Context) -> UINavigationController {
let preview = QLPreviewController()
preview.dataSource = context.coordinator
preview.navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .done,
target: context.coordinator,
action: #selector(Coordinator.done)
)
preview.navigationItem.leftBarButtonItem?.accessibilityIdentifier = "quick-look-done-button"
return UINavigationController(rootViewController: preview)
}
func updateUIViewController(_ controller: QLPreviewController, context: Context) {
func updateUIViewController(_ controller: UINavigationController, context: Context) {
context.coordinator.onDone = onDone
guard context.coordinator.url != url as NSURL else { return }
context.coordinator.url = url as NSURL
controller.reloadData()
(controller.viewControllers.first as? QLPreviewController)?.reloadData()
}
func makeCoordinator() -> Coordinator {
Coordinator(url: url as NSURL)
Coordinator(url: url as NSURL, onDone: onDone)
}
// MARK: - Coordinator
final class Coordinator: NSObject, QLPreviewControllerDataSource {
var url: NSURL
var onDone: () -> Void
init(url: NSURL) {
init(url: NSURL, onDone: @escaping () -> Void) {
self.url = url
self.onDone = onDone
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int { 1 }
@@ -44,5 +59,9 @@ struct QuickLookPreview: UIViewControllerRepresentable {
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
url
}
@objc func done() {
onDone()
}
}
}

View File

@@ -40,6 +40,50 @@ final class SystemExportButtonUITests: XCTestCase {
)
}
@MainActor
func testReportPreviewOffersActionButtons() throws {
let app = launch(arguments: ["--uitest-reset-data", "--uitest-sample-data"])
openFirstSystem(in: app)
let shareButton = app.buttons["system-overview-share-button"]
XCTAssertTrue(shareButton.waitForExistence(timeout: 15))
shareButton.tap()
let reportItem = app.buttons.matching(
NSPredicate(format: "label CONTAINS[c] 'PDF'")
).firstMatch
XCTAssertTrue(reportItem.waitForExistence(timeout: 10))
reportItem.tap()
// Quick Look lives in its own navigation stack; the Done button proves
// the navigation bar exists, the share item proves the file can leave
// the preview.
let doneButton = app.buttons["quick-look-done-button"]
XCTAssertTrue(
doneButton.waitForExistence(timeout: 60),
"Quick Look preview must be embedded in a navigation bar"
)
// Scope the action lookup to the preview's own navigation bar so the
// Overview share button underneath can never satisfy it.
let previewBar = app.navigationBars.containing(
.button,
identifier: "quick-look-done-button"
).firstMatch
XCTAssertTrue(previewBar.waitForExistence(timeout: 10))
let actionButtons = previewBar.buttons.allElementsBoundByIndex
.filter { $0.identifier != "quick-look-done-button" }
XCTAssertFalse(
actionButtons.isEmpty,
"Quick Look preview must offer an action button to share or open the file"
)
doneButton.tap()
XCTAssertTrue(shareButton.waitForExistence(timeout: 10))
}
// MARK: - Helpers
private func launch(arguments: [String]) -> XCUIApplication {