commit 81627c97567c4485c1fb772bca6d0135eef5c53e
parent 535e4411450cb470533d752522e438ef498a81ec
Author: Michael Camilleri <[email protected]>
Date: Fri, 17 Jul 2026 11:26:40 +0900
Promise the diagnostics log as data rather than a file
The promised file representation from the previous commit failed on
device: picking 'Save to Files' dismissed the share sheet without
saving, and share extensions could not load the attachment at all. The
share sheet fails to grant consumers a sandbox extension for a promised
file's URL, an acknowledged framework bug (see
https://developer.apple.com/forums/thread/714354).
This commit promises the log as plain-text data instead. The bytes cross
to the consumer directly and the consumer writes its own file, so no
sandbox handoff is involved. The file is still named via suggestedName,
and the render still runs only after the user picks an activity, keeping
the sheet's instant presentation.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat:
1 file changed, 14 insertions(+), 19 deletions(-)
diff --git a/Crossmate/Views/Settings/DiagnosticsView.swift b/Crossmate/Views/Settings/DiagnosticsView.swift
@@ -208,7 +208,7 @@ struct DiagnosticsView: View {
/// `UIActivityViewController` + a promised `NSItemProvider` has the semantics
/// the button needs: the sheet presents instantly (the configuration's
/// metadata provider titles the header without loading the item), and the
-/// file representation is loaded on a background queue only once the user
+/// promised representation is loaded on a background queue only once the user
/// picks an activity — the render cost lands inside the chosen activity's own
/// progress UI, where a wait reads as normal instead of broken.
///
@@ -262,36 +262,31 @@ private struct DiagnosticsShareButton: UIViewRepresentable {
}
}
-/// Writes the diagnostics file only after the user picks an activity in the
-/// share sheet — the promised file representation is not loaded before then.
+/// Renders the diagnostics dump only after the user picks an activity in the
+/// share sheet — the promised data representation is not loaded before then.
/// The dump inputs are snapshotted at tap time, so the file reflects the log
/// as the user saw it.
///
/// The item is a promise rather than a concrete `file:` URL on purpose:
/// Save to Files treats a raw URL item as "copy this exact file" and pins its
/// name, whereas a promised representation is a file the destination creates,
-/// so the sheet's rename field works. The name travels two ways — via
-/// `suggestedName`, and as the temp file's own last path component for
-/// consumers that ignore the suggestion.
+/// so the sheet's rename field works, with `suggestedName` carrying the name.
+///
+/// The promise must be *data*, not a file. The share sheet fails to grant
+/// consumers a sandbox extension for a promised file's URL (Apple DTS,
+/// developer.apple.com/forums/thread/714354), so a promised file
+/// representation makes Save to Files dismiss without saving and share
+/// extensions fail to load the attachment. A data promise crosses to the
+/// consumer directly and the consumer writes its own file, so no sandbox
+/// handoff is involved.
private enum DiagnosticsShareItem {
private static let fileName = "crossmate-diagnostics.txt"
- private static var fileURL: URL {
- FileManager.default.temporaryDirectory
- .appendingPathComponent(fileName)
- }
-
static func configuration(for dump: DiagnosticsDump) -> UIActivityItemsConfiguration {
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)
- }
+ provider.registerDataRepresentation(for: .plainText, visibility: .all) { completion in
+ completion(Data(dump.rendered().utf8), nil)
return nil
}