From 42d1107c53c67339eaae7e1ffd517b6c55156813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 13 Jan 2026 17:05:12 +0800 Subject: [PATCH] Fix profile save file not working SwiftUI only supports one fileExporter modifier per view hierarchy. When multiple fileExporter modifiers are attached, only the last one takes effect. Unified ProfileExportDocument and ProfileJSONExportDocument into a single ProfileAnyExportDocument to use one fileExporter for both file types. --- .../Views/Dashboard/Cards/ProfileCard.swift | 42 ++---- .../Dashboard/Cards/ProfilePickerSheet.swift | 120 +++++------------- Library/Database/Profile+Transferable.swift | 35 ++++- 3 files changed, 81 insertions(+), 116 deletions(-) diff --git a/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift b/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift index f1119ad..a43ff62 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift @@ -104,23 +104,12 @@ public struct ProfileCard: View { } } .fileExporter( - isPresented: $viewModel.showProfileExporter, - document: viewModel.profileExportDocument, - contentType: .profile, - defaultFilename: viewModel.profileExportDocument?.filename + isPresented: $viewModel.showExporter, + document: viewModel.exportDocument, + contentType: viewModel.exportDocument?.contentType ?? .data, + defaultFilename: viewModel.exportDocument?.filename ) { result in - viewModel.profileExportDocument = nil - if case let .failure(error) = result { - viewModel.alert = AlertState(error: error) - } - } - .fileExporter( - isPresented: $viewModel.showJSONExporter, - document: viewModel.profileJSONExportDocument, - contentType: .json, - defaultFilename: viewModel.profileJSONExportDocument?.filename - ) { result in - viewModel.profileJSONExportDocument = nil + viewModel.exportDocument = nil if case let .failure(error) = result { viewModel.alert = AlertState(error: error) } @@ -371,18 +360,13 @@ public struct ProfileCard: View { do { switch type { case .file: - viewModel.profileExportDocument = try ProfileExportDocument(content: profile.origin.toContent()) + let doc = try ProfileExportDocument(content: profile.origin.toContent()) + viewModel.exportDocument = ProfileAnyExportDocument(profile: doc) case .json: - viewModel.profileJSONExportDocument = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name) - } - DispatchQueue.main.async { - switch type { - case .file: - viewModel.showProfileExporter = true - case .json: - viewModel.showJSONExporter = true - } + let doc = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name) + viewModel.exportDocument = ProfileAnyExportDocument(json: doc) } + viewModel.showExporter = true } catch { viewModel.alert = AlertState(error: error) } @@ -495,10 +479,8 @@ extension ProfileCard { @Published var profileToEdit: Profile? @Published var shareItemType: ShareItemType? #if !os(tvOS) - @Published var profileExportDocument: ProfileExportDocument? - @Published var showProfileExporter = false - @Published var profileJSONExportDocument: ProfileJSONExportDocument? - @Published var showJSONExporter = false + @Published var exportDocument: ProfileAnyExportDocument? + @Published var showExporter = false #endif #if os(macOS) var shareButtonView: NSView? diff --git a/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift b/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift index 80d431f..73136ae 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift @@ -532,17 +532,11 @@ private struct ProfilePickerRow: View { #if os(macOS) @State private var shareItemType: ShareItemType? @State private var exportItemType: ExportItemType? - @State private var profileExportDocument: ProfileExportDocument? - @State private var showProfileExporter = false - @State private var profileJSONExportDocument: ProfileJSONExportDocument? - @State private var showJSONExporter = false @State private var menuAnchorView: NSView? #endif - #if os(iOS) - @State private var profileExportDocument: ProfileExportDocument? - @State private var showProfileExporter = false - @State private var profileJSONExportDocument: ProfileJSONExportDocument? - @State private var showJSONExporter = false + #if !os(tvOS) + @State private var exportDocument: ProfileAnyExportDocument? + @State private var showExporter = false #endif var body: some View { @@ -722,23 +716,12 @@ private struct ProfilePickerRow: View { } } .fileExporter( - isPresented: $showProfileExporter, - document: profileExportDocument, - contentType: .profile, - defaultFilename: profileExportDocument?.filename + isPresented: $showExporter, + document: exportDocument, + contentType: exportDocument?.contentType ?? .data, + defaultFilename: exportDocument?.filename ) { result in - profileExportDocument = nil - if case let .failure(error) = result { - alert = AlertState(error: error) - } - } - .fileExporter( - isPresented: $showJSONExporter, - document: profileJSONExportDocument, - contentType: .json, - defaultFilename: profileJSONExportDocument?.filename - ) { result in - profileJSONExportDocument = nil + exportDocument = nil if case let .failure(error) = result { alert = AlertState(error: error) } @@ -772,23 +755,12 @@ private struct ProfilePickerRow: View { } } .fileExporter( - isPresented: $showProfileExporter, - document: profileExportDocument, - contentType: .profile, - defaultFilename: profileExportDocument?.filename + isPresented: $showExporter, + document: exportDocument, + contentType: exportDocument?.contentType ?? .data, + defaultFilename: exportDocument?.filename ) { result in - profileExportDocument = nil - if case let .failure(error) = result { - alert = AlertState(error: error) - } - } - .fileExporter( - isPresented: $showJSONExporter, - document: profileJSONExportDocument, - contentType: .json, - defaultFilename: profileJSONExportDocument?.filename - ) { result in - profileJSONExportDocument = nil + exportDocument = nil if case let .failure(error) = result { alert = AlertState(error: error) } @@ -985,18 +957,13 @@ private struct ProfilePickerRow: View { do { switch type { case .file: - profileExportDocument = try ProfileExportDocument(content: profile.origin.toContent()) + let doc = try ProfileExportDocument(content: profile.origin.toContent()) + exportDocument = ProfileAnyExportDocument(profile: doc) case .json: - profileJSONExportDocument = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name) - } - DispatchQueue.main.async { - switch type { - case .file: - showProfileExporter = true - case .json: - showJSONExporter = true - } + let doc = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name) + exportDocument = ProfileAnyExportDocument(json: doc) } + showExporter = true } catch { alert = AlertState(error: error) } @@ -1052,12 +1019,13 @@ private struct ProfilePickerRow: View { do { switch type { case .file: - profileExportDocument = try ProfileExportDocument(content: profile.origin.toContent()) - showProfileExporter = true + let doc = try ProfileExportDocument(content: profile.origin.toContent()) + exportDocument = ProfileAnyExportDocument(profile: doc) case .json: - profileJSONExportDocument = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name) - showJSONExporter = true + let doc = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name) + exportDocument = ProfileAnyExportDocument(json: doc) } + showExporter = true } catch { alert = AlertState(error: error) } @@ -1167,10 +1135,8 @@ private struct ProfilePickerRow: View { @State private var isUpdating = false @State private var showQRCode = false @State private var showQRSShare = false - @State private var profileExportDocument: ProfileExportDocument? - @State private var showProfileExporter = false - @State private var profileJSONExportDocument: ProfileJSONExportDocument? - @State private var showJSONExporter = false + @State private var exportDocument: ProfileAnyExportDocument? + @State private var showExporter = false var body: some View { Group { @@ -1234,23 +1200,12 @@ private struct ProfilePickerRow: View { } } .fileExporter( - isPresented: $showProfileExporter, - document: profileExportDocument, - contentType: .profile, - defaultFilename: profileExportDocument?.filename + isPresented: $showExporter, + document: exportDocument, + contentType: exportDocument?.contentType ?? .data, + defaultFilename: exportDocument?.filename ) { result in - profileExportDocument = nil - if case let .failure(error) = result { - alert = AlertState(error: error) - } - } - .fileExporter( - isPresented: $showJSONExporter, - document: profileJSONExportDocument, - contentType: .json, - defaultFilename: profileJSONExportDocument?.filename - ) { result in - profileJSONExportDocument = nil + exportDocument = nil if case let .failure(error) = result { alert = AlertState(error: error) } @@ -1344,18 +1299,13 @@ private struct ProfilePickerRow: View { do { switch type { case .file: - profileExportDocument = try ProfileExportDocument(content: profile.origin.toContent()) + let doc = try ProfileExportDocument(content: profile.origin.toContent()) + exportDocument = ProfileAnyExportDocument(profile: doc) case .json: - profileJSONExportDocument = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name) - } - DispatchQueue.main.async { - switch type { - case .file: - showProfileExporter = true - case .json: - showJSONExporter = true - } + let doc = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name) + exportDocument = ProfileAnyExportDocument(json: doc) } + showExporter = true } catch { alert = AlertState(error: error) } diff --git a/Library/Database/Profile+Transferable.swift b/Library/Database/Profile+Transferable.swift index b9cd5b6..1569a4a 100644 --- a/Library/Database/Profile+Transferable.swift +++ b/Library/Database/Profile+Transferable.swift @@ -115,7 +115,7 @@ public struct TypedProfile: Transferable, Codable { } public extension UTType { - static var profile: UTType { .init(exportedAs: AppConfiguration.profileUTType) } + static let profile = UTType(exportedAs: AppConfiguration.profileUTType) } #if !os(tvOS) @@ -177,4 +177,37 @@ public extension UTType { return FileWrapper(regularFileWithContents: data) } } + + public struct ProfileAnyExportDocument: FileDocument { + public static var readableContentTypes: [UTType] { [.data, .json] } + + public let data: Data + public let filename: String + public let contentType: UTType + + public init(profile: ProfileExportDocument) { + data = profile.data + filename = profile.filename + contentType = .data + } + + public init(json: ProfileJSONExportDocument) { + data = json.content.data(using: .utf8) ?? Data() + filename = json.filename + contentType = .json + } + + public init(configuration: ReadConfiguration) throws { + guard let data = configuration.file.regularFileContents else { + throw CocoaError(.fileReadCorruptFile) + } + self.data = data + filename = "profile" + contentType = .data + } + + public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper { + FileWrapper(regularFileWithContents: data) + } + } #endif