crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 12646b23c31a4c4d39bd2d1c7dd11055771f2f62
parent a7c628265152091b8610e0c95899af859c4f3f75
Author: Michael Camilleri <[email protected]>
Date:   Wed,  8 Jul 2026 20:14:19 +0900

Fix diagnostics log sharing

Diffstat:
MCrossmate/Views/Settings/DiagnosticsView.swift | 62+++++++++++++++++++++++++++++++++++++++++---------------------
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) } }