diff --git a/Cable/Loads/LoadsView.swift b/Cable/Loads/LoadsView.swift index b90c61e..b36d6fd 100644 --- a/Cable/Loads/LoadsView.swift +++ b/Cable/Loads/LoadsView.swift @@ -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 diff --git a/Cable/Loads/QuickLookPreview.swift b/Cable/Loads/QuickLookPreview.swift index f396f58..844fd30 100644 --- a/Cable/Loads/QuickLookPreview.swift +++ b/Cable/Loads/QuickLookPreview.swift @@ -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() + } } } diff --git a/CableUITestsScreenshot/SystemExportButtonUITests.swift b/CableUITestsScreenshot/SystemExportButtonUITests.swift index 3db2fcd..83ecc35 100644 --- a/CableUITestsScreenshot/SystemExportButtonUITests.swift +++ b/CableUITestsScreenshot/SystemExportButtonUITests.swift @@ -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 {