From acbe7fc0a8bff8513b31c2d39e602576656db3d9 Mon Sep 17 00:00:00 2001 From: Reed Es Date: Fri, 13 Jan 2023 13:32:46 -0700 Subject: [PATCH 01/13] ongoing --- .../project.pbxproj | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj index cd51617..3b82035 100644 --- a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj +++ b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 40245EF6296BE7D4007B5DAB /* ExerciseRunList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */; }; 402A5F2F296B7FE000A43DB3 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402A5F2E296B7F7B00A43DB3 /* HistoryView.swift */; }; 402A5F32296B835B00A43DB3 /* Tabler in Frameworks */ = {isa = PBXBuildFile; productRef = 402A5F31296B835B00A43DB3 /* Tabler */; }; + 403CA2CE2971F6660084785B /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 403CA2CD2971F6660084785B /* ZIPFoundation */; }; 408FACFE296CAC9300D02C9C /* RoutineRunList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408FACFD296CABC400D02C9C /* RoutineRunList.swift */; }; 40B51E45294549DF0047377A /* GroutUI in Frameworks */ = {isa = PBXBuildFile; productRef = 40B51E44294549DF0047377A /* GroutUI */; }; 40CCF3F629454D2B007DDE69 /* GroutLib in Frameworks */ = {isa = PBXBuildFile; productRef = 40CCF3F529454D2B007DDE69 /* GroutLib */; }; @@ -47,6 +48,7 @@ 40E347D329452E4B003A19B9 /* Compactor in Frameworks */, 40EBE1CC29635306004B9189 /* GroutUI in Frameworks */, 40CCF3F629454D2B007DDE69 /* GroutLib in Frameworks */, + 403CA2CE2971F6660084785B /* ZIPFoundation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +127,7 @@ 40CCF3F529454D2B007DDE69 /* GroutLib */, 40EBE1CB29635306004B9189 /* GroutUI */, 402A5F31296B835B00A43DB3 /* Tabler */, + 403CA2CD2971F6660084785B /* ZIPFoundation */, ); productName = "Gym Routine Tracker Plus"; productReference = 40E347BA29452CF1003A19B9 /* Gym Routine Tracker Plus.app */; @@ -158,6 +161,7 @@ 40E347D129452E4B003A19B9 /* XCRemoteSwiftPackageReference "SwiftCompactor" */, 40EBE1CA29635306004B9189 /* XCRemoteSwiftPackageReference "GroutUI" */, 402A5F30296B835B00A43DB3 /* XCRemoteSwiftPackageReference "SwiftTabler" */, + 403CA2CC2971F6660084785B /* XCRemoteSwiftPackageReference "ZIPFoundation" */, ); productRefGroup = 40E347BB29452CF1003A19B9 /* Products */; projectDirPath = ""; @@ -424,6 +428,14 @@ minimumVersion = 0.9.6; }; }; + 403CA2CC2971F6660084785B /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/weichsel/ZIPFoundation.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.9; + }; + }; 40E347D129452E4B003A19B9 /* XCRemoteSwiftPackageReference "SwiftCompactor" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/openalloc/SwiftCompactor.git"; @@ -448,6 +460,11 @@ package = 402A5F30296B835B00A43DB3 /* XCRemoteSwiftPackageReference "SwiftTabler" */; productName = Tabler; }; + 403CA2CD2971F6660084785B /* ZIPFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = 403CA2CC2971F6660084785B /* XCRemoteSwiftPackageReference "ZIPFoundation" */; + productName = ZIPFoundation; + }; 40B51E44294549DF0047377A /* GroutUI */ = { isa = XCSwiftPackageProductDependency; productName = GroutUI; From 61b1be26404df4113e92b5623e42ffa403fa004d Mon Sep 17 00:00:00 2001 From: Reed Es Date: Fri, 13 Jan 2023 15:09:35 -0700 Subject: [PATCH 02/13] ongoing --- Sources/ContentView.swift | 74 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 945af76..71f9bdb 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -51,7 +51,7 @@ struct ContentView: View { NavStack(name: "settings", navData: $settingsNavData) { - SettingsForm() + SettingsForm(onExport: exportAction) } .tabItem { Label("Settings", systemImage: "gear") @@ -71,6 +71,78 @@ struct ContentView: View { Text("Routine Run not available to display detail.") } } + + // MARK: - Actions + + private func exportAction() { +// if let data = try? packageRebalance(params: ds.params, +// tradingAllocations: tradingAllocations, +// nonTradingAllocations: nonTradingAllocations, +// mpurchases: mpurchases, +// msales: msales) { +// #if os(macOS) +// NSSavePanel.saveData(data, name: "rebalance", ext: "zip", completion: { _ in }) +// #endif +// } + } + +// public func packageRebalance(params: BaseParams, +// tradingAllocations: [MRebalanceAllocation], +// nonTradingAllocations: [MRebalanceAllocation], +// mpurchases: [MRebalancePurchase], +// msales: [MRebalanceSale]) throws -> Data { +// guard let archive = Archive(accessMode: .create) +// else { throw FlowBaseError.archiveCreateFailure } +// +// let fileExt = rebalancePackageFormat.defaultFileExtension! +// +// let trading = try exportData(tradingAllocations, format: rebalancePackageFormat) +// try archive.addEntry(with: "trading-allocations.\(fileExt)", +// type: .file, +// uncompressedSize: Int64(trading.count), +// provider: { position, size -> Data in +// let range = Int(position) ..< Int(position) + size +// return trading.subdata(in: range) +// }) +// +// let nonTrading = try exportData(nonTradingAllocations, format: rebalancePackageFormat) +// try archive.addEntry(with: "non-trading-allocations.\(fileExt)", +// type: .file, +// uncompressedSize: Int64(nonTrading.count), +// provider: { position, size -> Data in +// let range = Int(position) ..< Int(position) + size +// return nonTrading.subdata(in: range) +// }) +// +// let exportedPurchases = try exportData(mpurchases, format: rebalancePackageFormat) +// try archive.addEntry(with: "purchases.\(fileExt)", +// type: .file, +// uncompressedSize: Int64(exportedPurchases.count), +// provider: { position, size -> Data in +// let range = Int(position) ..< Int(position) + size +// return exportedPurchases.subdata(in: range) +// }) +// +// let exportedSales = try exportData(msales, format: rebalancePackageFormat) +// try archive.addEntry(with: "sales.\(fileExt)", +// type: .file, +// uncompressedSize: Int64(exportedSales.count), +// provider: { position, size -> Data in +// let range = Int(position) ..< Int(position) + size +// return exportedSales.subdata(in: range) +// }) +// +// let paramsData: Data = try StorageManager.encodeToJSON(params) +// try archive.addEntry(with: "params.json", +// type: .file, +// uncompressedSize: Int64(paramsData.count), +// provider: { position, size -> Data in +// let range = Int(position) ..< Int(position) + size +// return paramsData.subdata(in: range) +// }) +// +// return archive.data! +// } } struct ContentView_Previews: PreviewProvider { From d7d83f65aa86b875ca2dc77c0a5dedb9229b1b11 Mon Sep 17 00:00:00 2001 From: Reed Es Date: Fri, 13 Jan 2023 16:51:32 -0700 Subject: [PATCH 03/13] ongoing --- .../project.pbxproj | 4 + Sources/ContentView.swift | 83 ++++--------------- Sources/Export-Utils.swift | 48 +++++++++++ 3 files changed, 68 insertions(+), 67 deletions(-) create mode 100644 Sources/Export-Utils.swift diff --git a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj index 3b82035..eb21cc1 100644 --- a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj +++ b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 402A5F2F296B7FE000A43DB3 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402A5F2E296B7F7B00A43DB3 /* HistoryView.swift */; }; 402A5F32296B835B00A43DB3 /* Tabler in Frameworks */ = {isa = PBXBuildFile; productRef = 402A5F31296B835B00A43DB3 /* Tabler */; }; 403CA2CE2971F6660084785B /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 403CA2CD2971F6660084785B /* ZIPFoundation */; }; + 403CA2D0297220460084785B /* Export-Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403CA2CF29721E750084785B /* Export-Utils.swift */; }; 408FACFE296CAC9300D02C9C /* RoutineRunList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408FACFD296CABC400D02C9C /* RoutineRunList.swift */; }; 40B51E45294549DF0047377A /* GroutUI in Frameworks */ = {isa = PBXBuildFile; productRef = 40B51E44294549DF0047377A /* GroutUI */; }; 40CCF3F629454D2B007DDE69 /* GroutLib in Frameworks */ = {isa = PBXBuildFile; productRef = 40CCF3F529454D2B007DDE69 /* GroutLib */; }; @@ -28,6 +29,7 @@ 4011EECE2961C9BC00A36D87 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseRunList.swift; sourceTree = ""; }; 402A5F2E296B7F7B00A43DB3 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; + 403CA2CF29721E750084785B /* Export-Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Export-Utils.swift"; sourceTree = ""; }; 408FACFD296CABC400D02C9C /* RoutineRunList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutineRunList.swift; sourceTree = ""; }; 40E347BA29452CF1003A19B9 /* Gym Routine Tracker Plus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Gym Routine Tracker Plus.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 40E347BD29452CF1003A19B9 /* PlusApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlusApp.swift; sourceTree = ""; }; @@ -89,6 +91,7 @@ 402A5F2E296B7F7B00A43DB3 /* HistoryView.swift */, 408FACFD296CABC400D02C9C /* RoutineRunList.swift */, 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */, + 403CA2CF29721E750084785B /* Export-Utils.swift */, 40E347C129452CF1003A19B9 /* Assets.xcassets */, 40EBCFDE294BFB880082A172 /* Info.plist */, 40E347C329452CF1003A19B9 /* App.entitlements */, @@ -191,6 +194,7 @@ buildActionMask = 2147483647; files = ( 40E347C029452CF1003A19B9 /* ContentView.swift in Sources */, + 403CA2D0297220460084785B /* Export-Utils.swift in Sources */, 408FACFE296CAC9300D02C9C /* RoutineRunList.swift in Sources */, 40245EF6296BE7D4007B5DAB /* ExerciseRunList.swift in Sources */, 402A5F2F296B7FE000A43DB3 /* HistoryView.swift in Sources */, diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 71f9bdb..1e927db 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -9,6 +9,7 @@ // import CoreData +import os import SwiftUI import GroutLib @@ -28,6 +29,9 @@ struct ContentView: View { @SceneStorage("main-history-nav") private var historyNavData: Data? @SceneStorage("main-settings-nav") private var settingsNavData: Data? + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: ContentView.self)) + var body: some View { TabView(selection: $selectedTab) { NavStack(name: "routines", @@ -75,74 +79,19 @@ struct ContentView: View { // MARK: - Actions private func exportAction() { -// if let data = try? packageRebalance(params: ds.params, -// tradingAllocations: tradingAllocations, -// nonTradingAllocations: nonTradingAllocations, -// mpurchases: mpurchases, -// msales: msales) { -// #if os(macOS) -// NSSavePanel.saveData(data, name: "rebalance", ext: "zip", completion: { _ in }) -// #endif -// } - } + do { + if let mainStore = PersistenceManager.getMainStore(viewContext), + let archiveStore = PersistenceManager.getArchiveStore(viewContext), + let data = try createZipArchive(viewContext, mainStore: mainStore, archiveStore: archiveStore) + { + print("data ready for export") -// public func packageRebalance(params: BaseParams, -// tradingAllocations: [MRebalanceAllocation], -// nonTradingAllocations: [MRebalanceAllocation], -// mpurchases: [MRebalancePurchase], -// msales: [MRebalanceSale]) throws -> Data { -// guard let archive = Archive(accessMode: .create) -// else { throw FlowBaseError.archiveCreateFailure } -// -// let fileExt = rebalancePackageFormat.defaultFileExtension! -// -// let trading = try exportData(tradingAllocations, format: rebalancePackageFormat) -// try archive.addEntry(with: "trading-allocations.\(fileExt)", -// type: .file, -// uncompressedSize: Int64(trading.count), -// provider: { position, size -> Data in -// let range = Int(position) ..< Int(position) + size -// return trading.subdata(in: range) -// }) -// -// let nonTrading = try exportData(nonTradingAllocations, format: rebalancePackageFormat) -// try archive.addEntry(with: "non-trading-allocations.\(fileExt)", -// type: .file, -// uncompressedSize: Int64(nonTrading.count), -// provider: { position, size -> Data in -// let range = Int(position) ..< Int(position) + size -// return nonTrading.subdata(in: range) -// }) -// -// let exportedPurchases = try exportData(mpurchases, format: rebalancePackageFormat) -// try archive.addEntry(with: "purchases.\(fileExt)", -// type: .file, -// uncompressedSize: Int64(exportedPurchases.count), -// provider: { position, size -> Data in -// let range = Int(position) ..< Int(position) + size -// return exportedPurchases.subdata(in: range) -// }) -// -// let exportedSales = try exportData(msales, format: rebalancePackageFormat) -// try archive.addEntry(with: "sales.\(fileExt)", -// type: .file, -// uncompressedSize: Int64(exportedSales.count), -// provider: { position, size -> Data in -// let range = Int(position) ..< Int(position) + size -// return exportedSales.subdata(in: range) -// }) -// -// let paramsData: Data = try StorageManager.encodeToJSON(params) -// try archive.addEntry(with: "params.json", -// type: .file, -// uncompressedSize: Int64(paramsData.count), -// provider: { position, size -> Data in -// let range = Int(position) ..< Int(position) + size -// return paramsData.subdata(in: range) -// }) -// -// return archive.data! -// } + /// NSSavePanel.saveData(data, name: "rebalance", ext: "zip", completion: { _ in }) + } + } catch { + logger.error("\(#function): \(error.localizedDescription)") + } + } } struct ContentView_Previews: PreviewProvider { diff --git a/Sources/Export-Utils.swift b/Sources/Export-Utils.swift new file mode 100644 index 0000000..8f5de76 --- /dev/null +++ b/Sources/Export-Utils.swift @@ -0,0 +1,48 @@ +// +// Export-Utils.swift +// +// Copyright 2023 OpenAlloc LLC +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// + +import CoreData +import ZIPFoundation + +import GroutLib + +public func createZipArchive(_ context: NSManagedObjectContext, + mainStore: NSPersistentStore, + archiveStore _: NSPersistentStore, + format: ExportFormat = .CSV) throws -> Data? +{ + guard let archive = Archive(accessMode: .create) + else { throw DataError.archiveCreationFailure } + + func blah(_: T.Type, + _ filePrefix: String, + inStore: NSPersistentStore?) throws + { + let request = makeRequest(T.self, inStore: inStore) + let results = try context.fetch(request) + let data = try exportData(results, format: format) + try archive.addEntry(with: "\(filePrefix).\(format.defaultFileExtension)", + type: .file, + uncompressedSize: Int64(data.count), + provider: { position, size -> Data in + let range = Int(position) ..< Int(position) + size + return data.subdata(in: range) + }) + } + + try blah(Routine.self, "routines", inStore: mainStore) + try blah(Exercise.self, "exercises", inStore: mainStore) + try blah(ZRoutine.self, "zroutines", inStore: mainStore) + try blah(ZRoutineRun.self, "zroutineruns", inStore: mainStore) + try blah(ZExercise.self, "zexercises", inStore: mainStore) + try blah(ZExerciseRun.self, "zexerciseruns", inStore: mainStore) + + return archive.data +} From e5427b1be0905a795fd537fe410211ea9edc0ffd Mon Sep 17 00:00:00 2001 From: Reed Es Date: Fri, 13 Jan 2023 23:25:26 -0700 Subject: [PATCH 04/13] ongoing --- Sources/ContentView.swift | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 1e927db..4ebd991 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -55,7 +55,7 @@ struct ContentView: View { NavStack(name: "settings", navData: $settingsNavData) { - SettingsForm(onExport: exportAction) + SettingsForm(bottom: bottom) } .tabItem { Label("Settings", systemImage: "gear") @@ -76,21 +76,43 @@ struct ContentView: View { } } + private func bottom() -> some View { + Section { + ShareLink(item: getData(), + subject: Text("subject"), message: Text("message"), preview: SharePreview("Zip")) { + Label("Export data", systemImage: "square.and.arrow.up") + } + } header: { + Text("Data Export") + .foregroundStyle(.tint) + } footer: { + Text("Exports to a zip file containing comma-separated-value (CSV) files, suitable for import into a spreadsheet.") + } + } + + // MARK: - Properties + + private var url: URL { + URL(string: "https://yahoo.com")! + } + // MARK: - Actions - private func exportAction() { + private func getData() -> Data { + logger.notice("\(#function) ENTER") do { if let mainStore = PersistenceManager.getMainStore(viewContext), let archiveStore = PersistenceManager.getArchiveStore(viewContext), let data = try createZipArchive(viewContext, mainStore: mainStore, archiveStore: archiveStore) { - print("data ready for export") - - /// NSSavePanel.saveData(data, name: "rebalance", ext: "zip", completion: { _ in }) + return data } } catch { logger.error("\(#function): \(error.localizedDescription)") } + + logger.notice("\(#function) EXIT") + return Data() } } From 144e90fc64174135362134c5e093451b6ad38d20 Mon Sep 17 00:00:00 2001 From: Reed Es Date: Fri, 13 Jan 2023 23:32:55 -0700 Subject: [PATCH 05/13] ongoing --- .../project.pbxproj | 4 ++ Sources/ContentView.swift | 42 +---------- Sources/Export-Utils.swift | 18 ++--- Sources/PhoneSettingsForm.swift | 72 +++++++++++++++++++ 4 files changed, 86 insertions(+), 50 deletions(-) create mode 100644 Sources/PhoneSettingsForm.swift diff --git a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj index eb21cc1..44c7359 100644 --- a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj +++ b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 402A5F32296B835B00A43DB3 /* Tabler in Frameworks */ = {isa = PBXBuildFile; productRef = 402A5F31296B835B00A43DB3 /* Tabler */; }; 403CA2CE2971F6660084785B /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 403CA2CD2971F6660084785B /* ZIPFoundation */; }; 403CA2D0297220460084785B /* Export-Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403CA2CF29721E750084785B /* Export-Utils.swift */; }; + 403CA2D22972829F0084785B /* PhoneSettingsForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403CA2D12972829F0084785B /* PhoneSettingsForm.swift */; }; 408FACFE296CAC9300D02C9C /* RoutineRunList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408FACFD296CABC400D02C9C /* RoutineRunList.swift */; }; 40B51E45294549DF0047377A /* GroutUI in Frameworks */ = {isa = PBXBuildFile; productRef = 40B51E44294549DF0047377A /* GroutUI */; }; 40CCF3F629454D2B007DDE69 /* GroutLib in Frameworks */ = {isa = PBXBuildFile; productRef = 40CCF3F529454D2B007DDE69 /* GroutLib */; }; @@ -30,6 +31,7 @@ 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseRunList.swift; sourceTree = ""; }; 402A5F2E296B7F7B00A43DB3 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; 403CA2CF29721E750084785B /* Export-Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Export-Utils.swift"; sourceTree = ""; }; + 403CA2D12972829F0084785B /* PhoneSettingsForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneSettingsForm.swift; sourceTree = ""; }; 408FACFD296CABC400D02C9C /* RoutineRunList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutineRunList.swift; sourceTree = ""; }; 40E347BA29452CF1003A19B9 /* Gym Routine Tracker Plus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Gym Routine Tracker Plus.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 40E347BD29452CF1003A19B9 /* PlusApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlusApp.swift; sourceTree = ""; }; @@ -91,6 +93,7 @@ 402A5F2E296B7F7B00A43DB3 /* HistoryView.swift */, 408FACFD296CABC400D02C9C /* RoutineRunList.swift */, 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */, + 403CA2D12972829F0084785B /* PhoneSettingsForm.swift */, 403CA2CF29721E750084785B /* Export-Utils.swift */, 40E347C129452CF1003A19B9 /* Assets.xcassets */, 40EBCFDE294BFB880082A172 /* Info.plist */, @@ -194,6 +197,7 @@ buildActionMask = 2147483647; files = ( 40E347C029452CF1003A19B9 /* ContentView.swift in Sources */, + 403CA2D22972829F0084785B /* PhoneSettingsForm.swift in Sources */, 403CA2D0297220460084785B /* Export-Utils.swift in Sources */, 408FACFE296CAC9300D02C9C /* RoutineRunList.swift in Sources */, 40245EF6296BE7D4007B5DAB /* ExerciseRunList.swift in Sources */, diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 4ebd991..e697ef7 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -8,7 +8,6 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. // -import CoreData import os import SwiftUI @@ -55,7 +54,7 @@ struct ContentView: View { NavStack(name: "settings", navData: $settingsNavData) { - SettingsForm(bottom: bottom) + PhoneSettingsForm() } .tabItem { Label("Settings", systemImage: "gear") @@ -75,45 +74,6 @@ struct ContentView: View { Text("Routine Run not available to display detail.") } } - - private func bottom() -> some View { - Section { - ShareLink(item: getData(), - subject: Text("subject"), message: Text("message"), preview: SharePreview("Zip")) { - Label("Export data", systemImage: "square.and.arrow.up") - } - } header: { - Text("Data Export") - .foregroundStyle(.tint) - } footer: { - Text("Exports to a zip file containing comma-separated-value (CSV) files, suitable for import into a spreadsheet.") - } - } - - // MARK: - Properties - - private var url: URL { - URL(string: "https://yahoo.com")! - } - - // MARK: - Actions - - private func getData() -> Data { - logger.notice("\(#function) ENTER") - do { - if let mainStore = PersistenceManager.getMainStore(viewContext), - let archiveStore = PersistenceManager.getArchiveStore(viewContext), - let data = try createZipArchive(viewContext, mainStore: mainStore, archiveStore: archiveStore) - { - return data - } - } catch { - logger.error("\(#function): \(error.localizedDescription)") - } - - logger.notice("\(#function) EXIT") - return Data() - } } struct ContentView_Previews: PreviewProvider { diff --git a/Sources/Export-Utils.swift b/Sources/Export-Utils.swift index 8f5de76..b7f25ef 100644 --- a/Sources/Export-Utils.swift +++ b/Sources/Export-Utils.swift @@ -21,9 +21,9 @@ public func createZipArchive(_ context: NSManagedObjectContext, guard let archive = Archive(accessMode: .create) else { throw DataError.archiveCreationFailure } - func blah(_: T.Type, - _ filePrefix: String, - inStore: NSPersistentStore?) throws + func makeDelimFile(_: T.Type, + _ filePrefix: String, + inStore: NSPersistentStore?) throws { let request = makeRequest(T.self, inStore: inStore) let results = try context.fetch(request) @@ -37,12 +37,12 @@ public func createZipArchive(_ context: NSManagedObjectContext, }) } - try blah(Routine.self, "routines", inStore: mainStore) - try blah(Exercise.self, "exercises", inStore: mainStore) - try blah(ZRoutine.self, "zroutines", inStore: mainStore) - try blah(ZRoutineRun.self, "zroutineruns", inStore: mainStore) - try blah(ZExercise.self, "zexercises", inStore: mainStore) - try blah(ZExerciseRun.self, "zexerciseruns", inStore: mainStore) + try makeDelimFile(Routine.self, "routines", inStore: mainStore) + try makeDelimFile(Exercise.self, "exercises", inStore: mainStore) + try makeDelimFile(ZRoutine.self, "zroutines", inStore: mainStore) + try makeDelimFile(ZRoutineRun.self, "zroutineruns", inStore: mainStore) + try makeDelimFile(ZExercise.self, "zexercises", inStore: mainStore) + try makeDelimFile(ZExerciseRun.self, "zexerciseruns", inStore: mainStore) return archive.data } diff --git a/Sources/PhoneSettingsForm.swift b/Sources/PhoneSettingsForm.swift new file mode 100644 index 0000000..c914bae --- /dev/null +++ b/Sources/PhoneSettingsForm.swift @@ -0,0 +1,72 @@ +// +// PhoneSettingsForm.swift +// +// Copyright 2023 OpenAlloc LLC +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// + +import CoreData +import os +import SwiftUI + +import GroutLib +import GroutUI + +struct PhoneSettingsForm: View { + @Environment(\.managedObjectContext) private var viewContext + + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: PhoneSettingsForm.self)) + + var body: some View { + SettingsForm(bottom: bottom) + } + + private func bottom() -> some View { + Section { + ShareLink(item: getData(), + subject: Text("subject"), message: Text("message"), preview: SharePreview("Zip")) { + Label("Export data", systemImage: "square.and.arrow.up") + } + } header: { + Text("Data Export") + .foregroundStyle(.tint) + } footer: { + Text("Exports to a zip file containing comma-separated-value (CSV) files, suitable for import into a spreadsheet.") + } + } + + // MARK: - Properties + + private var url: URL { + URL(string: "https://yahoo.com")! + } + + // MARK: - Actions + + private func getData() -> Data { + // logger.notice("\(#function) ENTER") + do { + if let mainStore = PersistenceManager.getMainStore(viewContext), + let archiveStore = PersistenceManager.getArchiveStore(viewContext), + let data = try createZipArchive(viewContext, mainStore: mainStore, archiveStore: archiveStore) + { + return data + } + } catch { + logger.error("\(#function): \(error.localizedDescription)") + } + + // logger.notice("\(#function) EXIT") + return Data() + } +} + +struct PhoneSettingsForm_Previews: PreviewProvider { + static var previews: some View { + PhoneSettingsForm() + } +} From cf774ceda2ff39c1959d1f6047a84043fd16aecc Mon Sep 17 00:00:00 2001 From: Reed Es Date: Fri, 13 Jan 2023 23:58:04 -0700 Subject: [PATCH 06/13] ongoing --- Sources/Export-Utils.swift | 24 +++++++++------ Sources/PhoneSettingsForm.swift | 52 +++++++++++++++++++++++---------- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/Sources/Export-Utils.swift b/Sources/Export-Utils.swift index b7f25ef..c0f4b01 100644 --- a/Sources/Export-Utils.swift +++ b/Sources/Export-Utils.swift @@ -15,20 +15,20 @@ import GroutLib public func createZipArchive(_ context: NSManagedObjectContext, mainStore: NSPersistentStore, - archiveStore _: NSPersistentStore, + archiveStore: NSPersistentStore, format: ExportFormat = .CSV) throws -> Data? { guard let archive = Archive(accessMode: .create) else { throw DataError.archiveCreationFailure } func makeDelimFile(_: T.Type, - _ filePrefix: String, inStore: NSPersistentStore?) throws { let request = makeRequest(T.self, inStore: inStore) let results = try context.fetch(request) let data = try exportData(results, format: format) - try archive.addEntry(with: "\(filePrefix).\(format.defaultFileExtension)", + let fileName = "\(T.fileNamePrefix).\(format.defaultFileExtension)" + try archive.addEntry(with: fileName, type: .file, uncompressedSize: Int64(data.count), provider: { position, size -> Data in @@ -37,12 +37,18 @@ public func createZipArchive(_ context: NSManagedObjectContext, }) } - try makeDelimFile(Routine.self, "routines", inStore: mainStore) - try makeDelimFile(Exercise.self, "exercises", inStore: mainStore) - try makeDelimFile(ZRoutine.self, "zroutines", inStore: mainStore) - try makeDelimFile(ZRoutineRun.self, "zroutineruns", inStore: mainStore) - try makeDelimFile(ZExercise.self, "zexercises", inStore: mainStore) - try makeDelimFile(ZExerciseRun.self, "zexerciseruns", inStore: mainStore) + try makeDelimFile(Routine.self, inStore: mainStore) + try makeDelimFile(Exercise.self, inStore: mainStore) + + // NOT WORKING: Local function 'makeDelimFile(_:inStore:)' requires that 'NSManagedObject' conform to 'Encodable' + // [ZRoutine.self, ZRoutineRun.self, ZExercise.self, ZExerciseRun.self].forEach { + // try makeDelimFile($0, inStore: archiveStore) + // } + + try makeDelimFile(ZRoutine.self, inStore: archiveStore) + try makeDelimFile(ZRoutineRun.self, inStore: archiveStore) + try makeDelimFile(ZExercise.self, inStore: archiveStore) + try makeDelimFile(ZExerciseRun.self, inStore: archiveStore) return archive.data } diff --git a/Sources/PhoneSettingsForm.swift b/Sources/PhoneSettingsForm.swift index c914bae..3404f52 100644 --- a/Sources/PhoneSettingsForm.swift +++ b/Sources/PhoneSettingsForm.swift @@ -17,50 +17,70 @@ import GroutUI struct PhoneSettingsForm: View { @Environment(\.managedObjectContext) private var viewContext + @EnvironmentObject private var router: MyRouter + + @AppStorage(colorSchemeModeKey) var colorSchemeMode: ColorSchemeMode = .automatic private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: PhoneSettingsForm.self)) var body: some View { - SettingsForm(bottom: bottom) - } + SettingsForm { + Section { + Picker("Color", selection: $colorSchemeMode) { + ForEach(ColorSchemeMode.allCases, id: \.self) { mode in + Text(mode.description).tag(mode) + } + } + .pickerStyle(SegmentedPickerStyle()) + } header: { + Text("Color Scheme") + .foregroundStyle(.tint) + } + + Section { + ShareLink(item: getData(), + subject: Text("subject"), message: Text("message"), preview: SharePreview("Zip")) { + Label("Export data", systemImage: "square.and.arrow.up") + } + } header: { + Text("Data Export") + .foregroundStyle(.tint) + } footer: { + Text("Exports to a zip file containing comma-separated-value (CSV) files, suitable for import into a spreadsheet.") + } - private func bottom() -> some View { - Section { - ShareLink(item: getData(), - subject: Text("subject"), message: Text("message"), preview: SharePreview("Zip")) { - Label("Export data", systemImage: "square.and.arrow.up") + Button(action: { + router.path.append(MyRoutes.about) + }) { + Text("About \(appName)") } - } header: { - Text("Data Export") - .foregroundStyle(.tint) - } footer: { - Text("Exports to a zip file containing comma-separated-value (CSV) files, suitable for import into a spreadsheet.") } } // MARK: - Properties - private var url: URL { - URL(string: "https://yahoo.com")! + private var appName: String { + Bundle.main.appName ?? "unknown" } // MARK: - Actions private func getData() -> Data { - // logger.notice("\(#function) ENTER") + logger.notice("\(#function) ENTER") do { if let mainStore = PersistenceManager.getMainStore(viewContext), let archiveStore = PersistenceManager.getArchiveStore(viewContext), let data = try createZipArchive(viewContext, mainStore: mainStore, archiveStore: archiveStore) { + logger.notice("\(#function) EXIT (success)") return data } } catch { logger.error("\(#function): \(error.localizedDescription)") } - // logger.notice("\(#function) EXIT") + logger.notice("\(#function) EXIT (failure)") return Data() } } From f479932fc86081e2b5ed51ec6f7c8f422aaa579b Mon Sep 17 00:00:00 2001 From: Reed Es Date: Sat, 14 Jan 2023 00:00:23 -0700 Subject: [PATCH 07/13] ongoing --- Sources/PhoneSettingsForm.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/PhoneSettingsForm.swift b/Sources/PhoneSettingsForm.swift index 3404f52..30b64bb 100644 --- a/Sources/PhoneSettingsForm.swift +++ b/Sources/PhoneSettingsForm.swift @@ -19,11 +19,15 @@ struct PhoneSettingsForm: View { @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject private var router: MyRouter + // MARK: - Locals + @AppStorage(colorSchemeModeKey) var colorSchemeMode: ColorSchemeMode = .automatic private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: PhoneSettingsForm.self)) + // MARK: - Views + var body: some View { SettingsForm { Section { From f3044fff2ff2ef0b41a56132ed463e6f5cb79009 Mon Sep 17 00:00:00 2001 From: Reed Es Date: Sat, 14 Jan 2023 00:59:34 -0700 Subject: [PATCH 08/13] ongoing --- Sources/PhoneSettingsForm.swift | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Sources/PhoneSettingsForm.swift b/Sources/PhoneSettingsForm.swift index 30b64bb..67b0e73 100644 --- a/Sources/PhoneSettingsForm.swift +++ b/Sources/PhoneSettingsForm.swift @@ -22,6 +22,7 @@ struct PhoneSettingsForm: View { // MARK: - Locals @AppStorage(colorSchemeModeKey) var colorSchemeMode: ColorSchemeMode = .automatic + @AppStorage(colorSchemeModeKey) var exportFormat: ExportFormat = .CSV private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: PhoneSettingsForm.self)) @@ -43,6 +44,19 @@ struct PhoneSettingsForm: View { } Section { + Picker(selection: $exportFormat) { + ForEach(ExportFormat.allCases, id: \.self) { mode in + // Text("\(mode.defaultFileExtension.uppercased()) (\(mode.rawValue))") + // Text("\(mode.description) (\(mode.defaultFileExtension))") + Text(mode.defaultFileExtension.uppercased()) + // Text(mode.description) + .tag(mode) + } + } label: { + Text("Format") + } + .pickerStyle(.segmented) + // .pickerStyle(MenuPickerStyle()) ShareLink(item: getData(), subject: Text("subject"), message: Text("message"), preview: SharePreview("Zip")) { Label("Export data", systemImage: "square.and.arrow.up") @@ -51,7 +65,7 @@ struct PhoneSettingsForm: View { Text("Data Export") .foregroundStyle(.tint) } footer: { - Text("Exports to a zip file containing comma-separated-value (CSV) files, suitable for import into a spreadsheet.") + Text("Exports to ZIP file as \(exportFormat.description) (\(exportFormat.rawValue)).") } Button(action: { @@ -75,7 +89,10 @@ struct PhoneSettingsForm: View { do { if let mainStore = PersistenceManager.getMainStore(viewContext), let archiveStore = PersistenceManager.getArchiveStore(viewContext), - let data = try createZipArchive(viewContext, mainStore: mainStore, archiveStore: archiveStore) + let data = try createZipArchive(viewContext, + mainStore: mainStore, + archiveStore: archiveStore, + format: exportFormat) { logger.notice("\(#function) EXIT (success)") return data From 299ca1179c0a234e01bdf2990a3f0572351cb73b Mon Sep 17 00:00:00 2001 From: Reed Es Date: Sat, 14 Jan 2023 01:04:23 -0700 Subject: [PATCH 09/13] ongoing --- Sources/PhoneSettingsForm.swift | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Sources/PhoneSettingsForm.swift b/Sources/PhoneSettingsForm.swift index 67b0e73..c822ef0 100644 --- a/Sources/PhoneSettingsForm.swift +++ b/Sources/PhoneSettingsForm.swift @@ -46,21 +46,17 @@ struct PhoneSettingsForm: View { Section { Picker(selection: $exportFormat) { ForEach(ExportFormat.allCases, id: \.self) { mode in - // Text("\(mode.defaultFileExtension.uppercased()) (\(mode.rawValue))") - // Text("\(mode.description) (\(mode.defaultFileExtension))") Text(mode.defaultFileExtension.uppercased()) - // Text(mode.description) .tag(mode) } } label: { - Text("Format") - } - .pickerStyle(.segmented) - // .pickerStyle(MenuPickerStyle()) - ShareLink(item: getData(), - subject: Text("subject"), message: Text("message"), preview: SharePreview("Zip")) { - Label("Export data", systemImage: "square.and.arrow.up") + ShareLink(item: getData(), + subject: Text("subject"), message: Text("message"), preview: SharePreview("Zip")) { + Label("Export data", systemImage: "square.and.arrow.up") + } } + .pickerStyle(MenuPickerStyle()) + } header: { Text("Data Export") .foregroundStyle(.tint) From b12ab1b6754bfdaaf0a1627c4d5593f75e697b72 Mon Sep 17 00:00:00 2001 From: Reed Es Date: Sat, 14 Jan 2023 11:34:28 -0700 Subject: [PATCH 10/13] ongoing --- Sources/PhoneSettingsForm.swift | 52 ++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/Sources/PhoneSettingsForm.swift b/Sources/PhoneSettingsForm.swift index c822ef0..40c8d35 100644 --- a/Sources/PhoneSettingsForm.swift +++ b/Sources/PhoneSettingsForm.swift @@ -27,6 +27,10 @@ struct PhoneSettingsForm: View { private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: PhoneSettingsForm.self)) + @State private var showFileExport = false + @State private var zipDocument: ZipDocument? + @State private var zipFileName: String? + // MARK: - Views var body: some View { @@ -44,24 +48,21 @@ struct PhoneSettingsForm: View { } Section { - Picker(selection: $exportFormat) { + Button(action: exportAction) { + Text("Export Data") + } + Picker("", selection: $exportFormat) { ForEach(ExportFormat.allCases, id: \.self) { mode in Text(mode.defaultFileExtension.uppercased()) .tag(mode) } - } label: { - ShareLink(item: getData(), - subject: Text("subject"), message: Text("message"), preview: SharePreview("Zip")) { - Label("Export data", systemImage: "square.and.arrow.up") - } } - .pickerStyle(MenuPickerStyle()) - + .pickerStyle(SegmentedPickerStyle()) } header: { Text("Data Export") .foregroundStyle(.tint) } footer: { - Text("Exports to ZIP file as \(exportFormat.description) (\(exportFormat.rawValue)).") + Text("Will export data to ZIP archive containing \(exportFormat.description) (\(exportFormat.rawValue)) files.") } Button(action: { @@ -70,6 +71,19 @@ struct PhoneSettingsForm: View { Text("About \(appName)") } } + .fileExporter(isPresented: $showFileExport, + document: zipDocument, + contentType: .zip, + defaultFilename: zipFileName) { result in + switch result { + case let .success(url): + logger.notice("\(#function): saved to \(url)") + case let .failure(error): + logger.error("\(#function): \(error.localizedDescription)") + } + zipDocument = nil + zipFileName = nil + } } // MARK: - Properties @@ -80,7 +94,19 @@ struct PhoneSettingsForm: View { // MARK: - Actions - private func getData() -> Data { + private func exportAction() { + if let document = createZipDocument() { + zipDocument = document + zipFileName = generateTimestampFileName(prefix: "grt-", suffix: ".zip") + showFileExport = true + } else { + logger.error("Unable to generate zip document, so not exporting.") + } + } + + // MARK: - Helpers + + private func createZipDocument() -> ZipDocument? { logger.notice("\(#function) ENTER") do { if let mainStore = PersistenceManager.getMainStore(viewContext), @@ -91,14 +117,14 @@ struct PhoneSettingsForm: View { format: exportFormat) { logger.notice("\(#function) EXIT (success)") - return data + return ZipDocument(data: data) } } catch { - logger.error("\(#function): \(error.localizedDescription)") + logger.error("\(#function): ERROR \(error.localizedDescription)") } logger.notice("\(#function) EXIT (failure)") - return Data() + return nil } } From c7117900137109c38c29c69e1f5b506d5e7fa963 Mon Sep 17 00:00:00 2001 From: Reed Es Date: Sat, 14 Jan 2023 12:13:12 -0700 Subject: [PATCH 11/13] ongoing --- .../project.pbxproj | 8 ++ Sources/ColorSettings.swift | 44 +++++++ Sources/ExportSettings.swift | 109 ++++++++++++++++++ Sources/PhoneSettingsForm.swift | 92 +-------------- 4 files changed, 163 insertions(+), 90 deletions(-) create mode 100644 Sources/ColorSettings.swift create mode 100644 Sources/ExportSettings.swift diff --git a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj index 44c7359..b42a189 100644 --- a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj +++ b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 4001ED0D29732D4500F165ED /* ExportSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4001ED0C29732D4500F165ED /* ExportSettings.swift */; }; + 4001ED0F29732E1300F165ED /* ColorSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4001ED0E29732E1300F165ED /* ColorSettings.swift */; }; 4011EECD2961C9B000A36D87 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 4011EECC2961C9B000A36D87 /* LICENSE */; }; 40245EF6296BE7D4007B5DAB /* ExerciseRunList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */; }; 402A5F2F296B7FE000A43DB3 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402A5F2E296B7F7B00A43DB3 /* HistoryView.swift */; }; @@ -26,6 +28,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 4001ED0C29732D4500F165ED /* ExportSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSettings.swift; sourceTree = ""; }; + 4001ED0E29732E1300F165ED /* ColorSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSettings.swift; sourceTree = ""; }; 4011EECC2961C9B000A36D87 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 4011EECE2961C9BC00A36D87 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseRunList.swift; sourceTree = ""; }; @@ -95,6 +99,8 @@ 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */, 403CA2D12972829F0084785B /* PhoneSettingsForm.swift */, 403CA2CF29721E750084785B /* Export-Utils.swift */, + 4001ED0C29732D4500F165ED /* ExportSettings.swift */, + 4001ED0E29732E1300F165ED /* ColorSettings.swift */, 40E347C129452CF1003A19B9 /* Assets.xcassets */, 40EBCFDE294BFB880082A172 /* Info.plist */, 40E347C329452CF1003A19B9 /* App.entitlements */, @@ -198,6 +204,8 @@ files = ( 40E347C029452CF1003A19B9 /* ContentView.swift in Sources */, 403CA2D22972829F0084785B /* PhoneSettingsForm.swift in Sources */, + 4001ED0F29732E1300F165ED /* ColorSettings.swift in Sources */, + 4001ED0D29732D4500F165ED /* ExportSettings.swift in Sources */, 403CA2D0297220460084785B /* Export-Utils.swift in Sources */, 408FACFE296CAC9300D02C9C /* RoutineRunList.swift in Sources */, 40245EF6296BE7D4007B5DAB /* ExerciseRunList.swift in Sources */, diff --git a/Sources/ColorSettings.swift b/Sources/ColorSettings.swift new file mode 100644 index 0000000..f9cddff --- /dev/null +++ b/Sources/ColorSettings.swift @@ -0,0 +1,44 @@ +// +// ColorSettings.swift +// +// Copyright 2023 OpenAlloc LLC +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// + +import SwiftUI + +import GroutLib +import GroutUI + +struct ColorSettings: View { + // MARK: - Locals + + @AppStorage(colorSchemeModeKey) var colorSchemeMode: ColorSchemeMode = .automatic + + // MARK: - Views + + var body: some View { + Section { + Picker("Color", selection: $colorSchemeMode) { + ForEach(ColorSchemeMode.allCases, id: \.self) { mode in + Text(mode.description).tag(mode) + } + } + .pickerStyle(SegmentedPickerStyle()) + } header: { + Text("Color Scheme") + .foregroundStyle(.tint) + } + } +} + +struct ColorSettings_Previews: PreviewProvider { + static var previews: some View { + Form { + ColorSettings() + } + } +} diff --git a/Sources/ExportSettings.swift b/Sources/ExportSettings.swift new file mode 100644 index 0000000..4d85374 --- /dev/null +++ b/Sources/ExportSettings.swift @@ -0,0 +1,109 @@ +// +// ExportSettings.swift +// +// Copyright 2023 OpenAlloc LLC +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// + +import CoreData +import os +import SwiftUI + +import GroutLib +import GroutUI + +struct ExportSettings: View { + @Environment(\.managedObjectContext) private var viewContext + + // MARK: - Locals + + @AppStorage(colorSchemeModeKey) var exportFormat: ExportFormat = .CSV + + @State private var showFileExport = false + @State private var zipDocument: ZipDocument? + @State private var zipFileName: String? + + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: ExportSettings.self)) + + // MARK: - Views + + var body: some View { + Section { + Button(action: exportAction) { + Text("Export Data") + } + Picker("", selection: $exportFormat) { + ForEach(ExportFormat.allCases, id: \.self) { mode in + Text(mode.defaultFileExtension.uppercased()) + .tag(mode) + } + } + .pickerStyle(SegmentedPickerStyle()) + } header: { + Text("Data Export") + .foregroundStyle(.tint) + } footer: { + Text("Will export data to ZIP archive containing \(exportFormat.description) (\(exportFormat.rawValue)) files.") + } + .fileExporter(isPresented: $showFileExport, + document: zipDocument, + contentType: .zip, + defaultFilename: zipFileName) { result in + switch result { + case let .success(url): + logger.notice("\(#function): saved to \(url)") + case let .failure(error): + logger.error("\(#function): \(error.localizedDescription)") + } + zipDocument = nil + zipFileName = nil + } + } + + // MARK: - Actions + + private func exportAction() { + if let document = createZipDocument() { + zipDocument = document + zipFileName = generateTimestampFileName(prefix: "grt-", suffix: ".zip") + showFileExport = true + } else { + logger.error("Unable to generate zip document, so not exporting.") + } + } + + // MARK: - Helpers + + private func createZipDocument() -> ZipDocument? { + logger.notice("\(#function) ENTER") + do { + if let mainStore = PersistenceManager.getMainStore(viewContext), + let archiveStore = PersistenceManager.getArchiveStore(viewContext), + let data = try createZipArchive(viewContext, + mainStore: mainStore, + archiveStore: archiveStore, + format: exportFormat) + { + logger.notice("\(#function) EXIT (success)") + return ZipDocument(data: data) + } + } catch { + logger.error("\(#function): ERROR \(error.localizedDescription)") + } + + logger.notice("\(#function) EXIT (failure)") + return nil + } +} + +struct ExportSettings_Previews: PreviewProvider { + static var previews: some View { + Form { + ExportSettings() + } + } +} diff --git a/Sources/PhoneSettingsForm.swift b/Sources/PhoneSettingsForm.swift index 40c8d35..6846b34 100644 --- a/Sources/PhoneSettingsForm.swift +++ b/Sources/PhoneSettingsForm.swift @@ -8,7 +8,6 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. // -import CoreData import os import SwiftUI @@ -16,54 +15,15 @@ import GroutLib import GroutUI struct PhoneSettingsForm: View { - @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject private var router: MyRouter - // MARK: - Locals - - @AppStorage(colorSchemeModeKey) var colorSchemeMode: ColorSchemeMode = .automatic - @AppStorage(colorSchemeModeKey) var exportFormat: ExportFormat = .CSV - - private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, - category: String(describing: PhoneSettingsForm.self)) - - @State private var showFileExport = false - @State private var zipDocument: ZipDocument? - @State private var zipFileName: String? - // MARK: - Views var body: some View { SettingsForm { - Section { - Picker("Color", selection: $colorSchemeMode) { - ForEach(ColorSchemeMode.allCases, id: \.self) { mode in - Text(mode.description).tag(mode) - } - } - .pickerStyle(SegmentedPickerStyle()) - } header: { - Text("Color Scheme") - .foregroundStyle(.tint) - } + ColorSettings() - Section { - Button(action: exportAction) { - Text("Export Data") - } - Picker("", selection: $exportFormat) { - ForEach(ExportFormat.allCases, id: \.self) { mode in - Text(mode.defaultFileExtension.uppercased()) - .tag(mode) - } - } - .pickerStyle(SegmentedPickerStyle()) - } header: { - Text("Data Export") - .foregroundStyle(.tint) - } footer: { - Text("Will export data to ZIP archive containing \(exportFormat.description) (\(exportFormat.rawValue)) files.") - } + ExportSettings() Button(action: { router.path.append(MyRoutes.about) @@ -71,19 +31,6 @@ struct PhoneSettingsForm: View { Text("About \(appName)") } } - .fileExporter(isPresented: $showFileExport, - document: zipDocument, - contentType: .zip, - defaultFilename: zipFileName) { result in - switch result { - case let .success(url): - logger.notice("\(#function): saved to \(url)") - case let .failure(error): - logger.error("\(#function): \(error.localizedDescription)") - } - zipDocument = nil - zipFileName = nil - } } // MARK: - Properties @@ -91,41 +38,6 @@ struct PhoneSettingsForm: View { private var appName: String { Bundle.main.appName ?? "unknown" } - - // MARK: - Actions - - private func exportAction() { - if let document = createZipDocument() { - zipDocument = document - zipFileName = generateTimestampFileName(prefix: "grt-", suffix: ".zip") - showFileExport = true - } else { - logger.error("Unable to generate zip document, so not exporting.") - } - } - - // MARK: - Helpers - - private func createZipDocument() -> ZipDocument? { - logger.notice("\(#function) ENTER") - do { - if let mainStore = PersistenceManager.getMainStore(viewContext), - let archiveStore = PersistenceManager.getArchiveStore(viewContext), - let data = try createZipArchive(viewContext, - mainStore: mainStore, - archiveStore: archiveStore, - format: exportFormat) - { - logger.notice("\(#function) EXIT (success)") - return ZipDocument(data: data) - } - } catch { - logger.error("\(#function): ERROR \(error.localizedDescription)") - } - - logger.notice("\(#function) EXIT (failure)") - return nil - } } struct PhoneSettingsForm_Previews: PreviewProvider { From abc52713102c97c515c87664731163a30901b18a Mon Sep 17 00:00:00 2001 From: Reed Es Date: Sat, 14 Jan 2023 12:14:29 -0700 Subject: [PATCH 12/13] ongoing --- Sources/ExportSettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ExportSettings.swift b/Sources/ExportSettings.swift index 4d85374..5e4d75d 100644 --- a/Sources/ExportSettings.swift +++ b/Sources/ExportSettings.swift @@ -20,7 +20,7 @@ struct ExportSettings: View { // MARK: - Locals - @AppStorage(colorSchemeModeKey) var exportFormat: ExportFormat = .CSV + @AppStorage(exportFormatKey) var exportFormat: ExportFormat = .CSV @State private var showFileExport = false @State private var zipDocument: ZipDocument? From 69ce8ce4ed08502dda37c3d0e369e8bfeb31a4e2 Mon Sep 17 00:00:00 2001 From: Reed Es Date: Sat, 14 Jan 2023 12:20:51 -0700 Subject: [PATCH 13/13] ongoing --- .../project.pbxproj | 38 ++++++++++++++----- Sources/{ => Helper}/Export-Utils.swift | 0 Sources/Helper/ZipDocument.swift | 32 ++++++++++++++++ Sources/{ => Views}/ColorSettings.swift | 0 Sources/{ => Views}/ContentView.swift | 0 Sources/{ => Views}/ExerciseRunList.swift | 0 Sources/{ => Views}/ExportSettings.swift | 0 Sources/{ => Views}/HistoryView.swift | 0 Sources/{ => Views}/PhoneSettingsForm.swift | 0 Sources/{ => Views}/RoutineRunList.swift | 0 10 files changed, 61 insertions(+), 9 deletions(-) rename Sources/{ => Helper}/Export-Utils.swift (100%) create mode 100644 Sources/Helper/ZipDocument.swift rename Sources/{ => Views}/ColorSettings.swift (100%) rename Sources/{ => Views}/ContentView.swift (100%) rename Sources/{ => Views}/ExerciseRunList.swift (100%) rename Sources/{ => Views}/ExportSettings.swift (100%) rename Sources/{ => Views}/HistoryView.swift (100%) rename Sources/{ => Views}/PhoneSettingsForm.swift (100%) rename Sources/{ => Views}/RoutineRunList.swift (100%) diff --git a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj index b42a189..a8b2398 100644 --- a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj +++ b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 4001ED0D29732D4500F165ED /* ExportSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4001ED0C29732D4500F165ED /* ExportSettings.swift */; }; 4001ED0F29732E1300F165ED /* ColorSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4001ED0E29732E1300F165ED /* ColorSettings.swift */; }; + 401064D92973374700BFB00E /* ZipDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401064D82973374700BFB00E /* ZipDocument.swift */; }; 4011EECD2961C9B000A36D87 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 4011EECC2961C9B000A36D87 /* LICENSE */; }; 40245EF6296BE7D4007B5DAB /* ExerciseRunList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */; }; 402A5F2F296B7FE000A43DB3 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402A5F2E296B7F7B00A43DB3 /* HistoryView.swift */; }; @@ -30,6 +31,7 @@ /* Begin PBXFileReference section */ 4001ED0C29732D4500F165ED /* ExportSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSettings.swift; sourceTree = ""; }; 4001ED0E29732E1300F165ED /* ColorSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSettings.swift; sourceTree = ""; }; + 401064D82973374700BFB00E /* ZipDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipDocument.swift; sourceTree = ""; }; 4011EECC2961C9B000A36D87 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 4011EECE2961C9BC00A36D87 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseRunList.swift; sourceTree = ""; }; @@ -63,6 +65,29 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 401064DA2973378500BFB00E /* Helper */ = { + isa = PBXGroup; + children = ( + 401064D82973374700BFB00E /* ZipDocument.swift */, + 403CA2CF29721E750084785B /* Export-Utils.swift */, + ); + path = Helper; + sourceTree = ""; + }; + 401064DB2973378C00BFB00E /* Views */ = { + isa = PBXGroup; + children = ( + 40E347BF29452CF1003A19B9 /* ContentView.swift */, + 4001ED0E29732E1300F165ED /* ColorSettings.swift */, + 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */, + 4001ED0C29732D4500F165ED /* ExportSettings.swift */, + 402A5F2E296B7F7B00A43DB3 /* HistoryView.swift */, + 403CA2D12972829F0084785B /* PhoneSettingsForm.swift */, + 408FACFD296CABC400D02C9C /* RoutineRunList.swift */, + ); + path = Views; + sourceTree = ""; + }; 40B51E41294549D90047377A /* Frameworks */ = { isa = PBXGroup; children = ( @@ -93,17 +118,11 @@ isa = PBXGroup; children = ( 40E347BD29452CF1003A19B9 /* PlusApp.swift */, - 40E347BF29452CF1003A19B9 /* ContentView.swift */, - 402A5F2E296B7F7B00A43DB3 /* HistoryView.swift */, - 408FACFD296CABC400D02C9C /* RoutineRunList.swift */, - 40245EF5296BE7D4007B5DAB /* ExerciseRunList.swift */, - 403CA2D12972829F0084785B /* PhoneSettingsForm.swift */, - 403CA2CF29721E750084785B /* Export-Utils.swift */, - 4001ED0C29732D4500F165ED /* ExportSettings.swift */, - 4001ED0E29732E1300F165ED /* ColorSettings.swift */, + 401064DB2973378C00BFB00E /* Views */, + 401064DA2973378500BFB00E /* Helper */, + 40E347C329452CF1003A19B9 /* App.entitlements */, 40E347C129452CF1003A19B9 /* Assets.xcassets */, 40EBCFDE294BFB880082A172 /* Info.plist */, - 40E347C329452CF1003A19B9 /* App.entitlements */, 40E347C429452CF1003A19B9 /* Preview Content */, ); path = Sources; @@ -206,6 +225,7 @@ 403CA2D22972829F0084785B /* PhoneSettingsForm.swift in Sources */, 4001ED0F29732E1300F165ED /* ColorSettings.swift in Sources */, 4001ED0D29732D4500F165ED /* ExportSettings.swift in Sources */, + 401064D92973374700BFB00E /* ZipDocument.swift in Sources */, 403CA2D0297220460084785B /* Export-Utils.swift in Sources */, 408FACFE296CAC9300D02C9C /* RoutineRunList.swift in Sources */, 40245EF6296BE7D4007B5DAB /* ExerciseRunList.swift in Sources */, diff --git a/Sources/Export-Utils.swift b/Sources/Helper/Export-Utils.swift similarity index 100% rename from Sources/Export-Utils.swift rename to Sources/Helper/Export-Utils.swift diff --git a/Sources/Helper/ZipDocument.swift b/Sources/Helper/ZipDocument.swift new file mode 100644 index 0000000..6f0a35b --- /dev/null +++ b/Sources/Helper/ZipDocument.swift @@ -0,0 +1,32 @@ +// +// ZipDocument.swift +// +// Copyright 2022, 2023 OpenAlloc LLC +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// + +import SwiftUI +import UniformTypeIdentifiers + +/// Simple wrapper for Zip Archive data +public struct ZipDocument: FileDocument { + public static var readableContentTypes = [UTType.zip] + public static var writableContentTypes = [UTType.zip] + + private var data: Data + + public init(data: Data) { + self.data = data + } + + public init(configuration: ReadConfiguration) throws { + data = configuration.file.regularFileContents ?? Data() + } + + public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper { + FileWrapper(regularFileWithContents: data) + } +} diff --git a/Sources/ColorSettings.swift b/Sources/Views/ColorSettings.swift similarity index 100% rename from Sources/ColorSettings.swift rename to Sources/Views/ColorSettings.swift diff --git a/Sources/ContentView.swift b/Sources/Views/ContentView.swift similarity index 100% rename from Sources/ContentView.swift rename to Sources/Views/ContentView.swift diff --git a/Sources/ExerciseRunList.swift b/Sources/Views/ExerciseRunList.swift similarity index 100% rename from Sources/ExerciseRunList.swift rename to Sources/Views/ExerciseRunList.swift diff --git a/Sources/ExportSettings.swift b/Sources/Views/ExportSettings.swift similarity index 100% rename from Sources/ExportSettings.swift rename to Sources/Views/ExportSettings.swift diff --git a/Sources/HistoryView.swift b/Sources/Views/HistoryView.swift similarity index 100% rename from Sources/HistoryView.swift rename to Sources/Views/HistoryView.swift diff --git a/Sources/PhoneSettingsForm.swift b/Sources/Views/PhoneSettingsForm.swift similarity index 100% rename from Sources/PhoneSettingsForm.swift rename to Sources/Views/PhoneSettingsForm.swift diff --git a/Sources/RoutineRunList.swift b/Sources/Views/RoutineRunList.swift similarity index 100% rename from Sources/RoutineRunList.swift rename to Sources/Views/RoutineRunList.swift