commit 12646b23c31a4c4d39bd2d1c7dd11055771f2f62
parent a7c628265152091b8610e0c95899af859c4f3f75
Author: Michael Camilleri <[email protected]>
Date: Wed, 8 Jul 2026 20:14:19 +0900
Fix diagnostics log sharing
Diffstat:
1 file changed, 41 insertions(+), 21 deletions(-)
diff --git a/Crossmate/Views/Settings/DiagnosticsView.swift b/Crossmate/Views/Settings/DiagnosticsView.swift
@@ -1,7 +1,7 @@
import CloudKit
+import LinkPresentation
import SwiftUI
import UIKit
-import UniformTypeIdentifiers
private enum TimestampTimeZone {
case local
@@ -204,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` + a promised `NSItemProvider` has the semantics the
+/// `UIActivityViewController` + `UIActivityItemProvider` 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
@@ -229,9 +229,11 @@ private struct DiagnosticsShareButton: UIViewRepresentable {
UIAction { [snapshot, weak button] _ in
MainActor.assumeIsolated {
guard let button else { return }
- let provider = DiagnosticsShareItem.provider(for: snapshot())
- let configuration = UIActivityItemsConfiguration(itemProviders: [provider])
- let controller = UIActivityViewController(activityItemsConfiguration: configuration)
+ let provider = DiagnosticsActivityItemProvider(dump: snapshot())
+ let controller = UIActivityViewController(
+ activityItems: [provider],
+ applicationActivities: nil
+ )
// iPad presents the share sheet as a popover and traps
// without an anchor.
if let popover = controller.popoverPresentationController {
@@ -262,9 +264,10 @@ private struct DiagnosticsShareButton: UIViewRepresentable {
}
/// Writes the diagnostics file only after the user picks an activity in the
-/// share sheet. The dump inputs are snapshotted at tap time, so the file
-/// reflects the log as the user saw it.
-private enum DiagnosticsShareItem {
+/// 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 {
private static let fileName = "crossmate-diagnostics.txt"
private static var fileURL: URL {
@@ -272,19 +275,36 @@ private enum DiagnosticsShareItem {
.appendingPathComponent(fileName)
}
- 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
+ private let dump: DiagnosticsDump
+
+ init(dump: DiagnosticsDump) {
+ self.dump = dump
+ Self.ensurePlaceholderFile()
+ 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()
}
- return provider
+ }
+
+ override func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
+ let metadata = LPLinkMetadata()
+ metadata.originalURL = Self.fileURL
+ metadata.url = Self.fileURL
+ metadata.title = Self.fileName
+ return metadata
+ }
+
+ private static func ensurePlaceholderFile() {
+ guard !FileManager.default.fileExists(atPath: fileURL.path) else { return }
+ try? "".write(to: fileURL, atomically: true, encoding: .utf8)
}
}