crossmate

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

commit 97780727b16e897aa7feb550056d61ca4e9d7077
parent 07283e311d3bde56a9c45d9a34a3187d18473c38
Author: Michael Camilleri <[email protected]>
Date:   Wed,  8 Jul 2026 19:52:32 +0900

Suggest filename for diagnostics log

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