commit 97780727b16e897aa7feb550056d61ca4e9d7077
parent 07283e311d3bde56a9c45d9a34a3187d18473c38
Author: Michael Camilleri <[email protected]>
Date: Wed, 8 Jul 2026 19:52:32 +0900
Suggest filename for diagnostics log
Diffstat:
1 file changed, 24 insertions(+), 27 deletions(-)
diff --git a/Crossmate/Views/Settings/DiagnosticsView.swift b/Crossmate/Views/Settings/DiagnosticsView.swift
@@ -1,6 +1,7 @@
import CloudKit
import SwiftUI
import UIKit
+import UniformTypeIdentifiers
private enum TimestampTimeZone {
case local
@@ -203,7 +204,7 @@ struct DiagnosticsView: View {
/// The share button, in UIKit deliberately. SwiftUI's `ShareLink` resolves its
/// item — for a file, render *and* disk write — before it will present the
/// sheet, which put a count-the-seconds stall between the tap and the menu.
-/// `UIActivityViewController` + `UIActivityItemProvider` has the semantics the
+/// `UIActivityViewController` + a promised `NSItemProvider` has the semantics the
/// button needs: the sheet presents instantly against a placeholder, and the
/// provider is consulted on a background operation only once the user picks
/// an activity — the render cost lands inside the chosen activity's own
@@ -228,11 +229,9 @@ private struct DiagnosticsShareButton: UIViewRepresentable {
UIAction { [snapshot, weak button] _ in
MainActor.assumeIsolated {
guard let button else { return }
- let provider = DiagnosticsActivityItemProvider(dump: snapshot())
- let controller = UIActivityViewController(
- activityItems: [provider],
- applicationActivities: nil
- )
+ let provider = DiagnosticsShareItem.provider(for: snapshot())
+ let configuration = UIActivityItemsConfiguration(itemProviders: [provider])
+ let controller = UIActivityViewController(activityItemsConfiguration: configuration)
// iPad presents the share sheet as a popover and traps
// without an anchor.
if let popover = controller.popoverPresentationController {
@@ -263,31 +262,29 @@ private struct DiagnosticsShareButton: UIViewRepresentable {
}
/// Writes the diagnostics file only after the user picks an activity in the
-/// share sheet — `UIActivityViewController` calls `item` on a background
-/// operation at that point, never before the sheet is up. The dump inputs are
-/// snapshotted at tap time, so the file reflects the log as the user saw it.
-private final class DiagnosticsActivityItemProvider: UIActivityItemProvider, @unchecked Sendable {
+/// share sheet. The dump inputs are snapshotted at tap time, so the file
+/// reflects the log as the user saw it.
+private enum DiagnosticsShareItem {
+ private static let fileName = "crossmate-diagnostics.txt"
+
private static var fileURL: URL {
FileManager.default.temporaryDirectory
- .appendingPathComponent("crossmate-diagnostics.txt")
+ .appendingPathComponent(fileName)
}
- private let dump: DiagnosticsDump
-
- init(dump: DiagnosticsDump) {
- self.dump = dump
- super.init(placeholderItem: Self.fileURL)
- }
-
- override var item: Any {
- let url = Self.fileURL
- do {
- try dump.rendered().write(to: url, atomically: true, encoding: .utf8)
- return url
- } catch {
- // No file, but the rendered text still gives the tester
- // something to send.
- return dump.rendered()
+ static func provider(for dump: DiagnosticsDump) -> NSItemProvider {
+ let provider = NSItemProvider()
+ provider.suggestedName = fileName
+ provider.registerFileRepresentation(for: .plainText, visibility: .all) { completion in
+ do {
+ let url = fileURL
+ try dump.rendered().write(to: url, atomically: true, encoding: .utf8)
+ completion(url, false, nil)
+ } catch {
+ completion(nil, false, error)
+ }
+ return nil
}
+ return provider
}
}