diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..1beab21 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,7 @@ +--wraparguments after-first + +# was changing +# let routine: Routine = Routine.get(viewContext, forURIRepresentation: routineURI) +# to +# let routine = Routine.get(viewContext, forURIRepresentation: routineURI) +--disable redundantType diff --git a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj index d0fb35d..142e572 100644 --- a/Gym Routine Tracker Plus.xcodeproj/project.pbxproj +++ b/Gym Routine Tracker Plus.xcodeproj/project.pbxproj @@ -7,15 +7,11 @@ 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 */; }; - 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 */; }; 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 */; }; @@ -29,14 +25,10 @@ /* 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 = ""; }; - 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 = ""; }; 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; }; @@ -65,22 +57,11 @@ /* 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 */, @@ -119,7 +100,6 @@ children = ( 40E347BD29452CF1003A19B9 /* PlusApp.swift */, 401064DB2973378C00BFB00E /* Views */, - 401064DA2973378500BFB00E /* Helper */, 40E347C329452CF1003A19B9 /* App.entitlements */, 40E347C129452CF1003A19B9 /* Assets.xcassets */, 40EBCFDE294BFB880082A172 /* Info.plist */, @@ -172,7 +152,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1410; - LastUpgradeCheck = 1410; + LastUpgradeCheck = 1420; TargetAttributes = { 40E347B929452CF1003A19B9 = { CreatedOnToolsVersion = 14.1; @@ -223,10 +203,6 @@ files = ( 40E347C029452CF1003A19B9 /* ContentView.swift in Sources */, 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 */, 402A5F2F296B7FE000A43DB3 /* HistoryView.swift in Sources */, @@ -270,6 +246,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -328,6 +305,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -352,15 +330,18 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Sources/App.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 10; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; DEVELOPMENT_TEAM = J735QC5U38; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Sources/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Gym RT+"; + INFOPLIST_KEY_CFBundleDisplayName = "Gym Routines"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; @@ -376,7 +357,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.6.1; + MARKETING_VERSION = 1.7.0; PRODUCT_BUNDLE_IDENTIFIER = org.openalloc.grout.plus; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -395,15 +376,18 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Sources/App.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 10; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; DEVELOPMENT_TEAM = J735QC5U38; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Sources/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Gym RT+"; + INFOPLIST_KEY_CFBundleDisplayName = "Gym Routines"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; @@ -419,7 +403,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.6.1; + MARKETING_VERSION = 1.7.0; PRODUCT_BUNDLE_IDENTIFIER = org.openalloc.grout.plus; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; diff --git a/Gym Routine Tracker Plus.xcodeproj/xcshareddata/xcschemes/Gym Routine Tracker Plus.xcscheme b/Gym Routine Tracker Plus.xcodeproj/xcshareddata/xcschemes/Gym Routine Tracker Plus.xcscheme index ad4ba6c..d61b4aa 100644 --- a/Gym Routine Tracker Plus.xcodeproj/xcshareddata/xcschemes/Gym Routine Tracker Plus.xcscheme +++ b/Gym Routine Tracker Plus.xcodeproj/xcshareddata/xcschemes/Gym Routine Tracker Plus.xcscheme @@ -1,6 +1,6 @@ Gym Routine Tracker Plus.xcscheme_^#shared#^_ orderHint - 0 + 1 SuppressBuildableAutocreation diff --git a/Sources/Assets.xcassets/grt_icon.imageset/Contents.json b/Sources/Assets.xcassets/app_icon.imageset/Contents.json similarity index 100% rename from Sources/Assets.xcassets/grt_icon.imageset/Contents.json rename to Sources/Assets.xcassets/app_icon.imageset/Contents.json diff --git a/Sources/Assets.xcassets/grt_icon.imageset/grt_logo_120.png b/Sources/Assets.xcassets/app_icon.imageset/grt_logo_120.png similarity index 100% rename from Sources/Assets.xcassets/grt_icon.imageset/grt_logo_120.png rename to Sources/Assets.xcassets/app_icon.imageset/grt_logo_120.png diff --git a/Sources/Assets.xcassets/grt_icon.imageset/grt_logo_180.png b/Sources/Assets.xcassets/app_icon.imageset/grt_logo_180.png similarity index 100% rename from Sources/Assets.xcassets/grt_icon.imageset/grt_logo_180.png rename to Sources/Assets.xcassets/app_icon.imageset/grt_logo_180.png diff --git a/Sources/Assets.xcassets/grt_icon.imageset/grt_logo_60.png b/Sources/Assets.xcassets/app_icon.imageset/grt_logo_60.png similarity index 100% rename from Sources/Assets.xcassets/grt_icon.imageset/grt_logo_60.png rename to Sources/Assets.xcassets/app_icon.imageset/grt_logo_60.png diff --git a/Sources/Helper/Export-Utils.swift b/Sources/Helper/Export-Utils.swift deleted file mode 100644 index c0f4b01..0000000 --- a/Sources/Helper/Export-Utils.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// 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 makeDelimFile(_: T.Type, - inStore: NSPersistentStore?) throws - { - let request = makeRequest(T.self, inStore: inStore) - let results = try context.fetch(request) - let data = try exportData(results, format: format) - let fileName = "\(T.fileNamePrefix).\(format.defaultFileExtension)" - try archive.addEntry(with: fileName, - type: .file, - uncompressedSize: Int64(data.count), - provider: { position, size -> Data in - let range = Int(position) ..< Int(position) + size - return data.subdata(in: range) - }) - } - - 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/Helper/ZipDocument.swift b/Sources/Helper/ZipDocument.swift deleted file mode 100644 index 6f0a35b..0000000 --- a/Sources/Helper/ZipDocument.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// 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/PlusApp.swift b/Sources/PlusApp.swift index fc93358..3238798 100644 --- a/Sources/PlusApp.swift +++ b/Sources/PlusApp.swift @@ -13,31 +13,35 @@ import SwiftUI import GroutLib import GroutUI +import TrackerLib +import TrackerUI @main struct Gym_Routine_Tracker_Plus_App: App { - let persistenceManager = PersistenceManager.shared + @Environment(\.scenePhase) var scenePhase + + // MARK: - Locals + + private let coreDataStack = CoreDataStack(isCloud: true) @AppStorage(colorSchemeModeKey) var colorSchemeMode: ColorSchemeMode = .automatic - @Environment(\.scenePhase) var scenePhase + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, + category: "App") - private let logger = Logger( - subsystem: Bundle.main.bundleIdentifier!, - category: "App" - ) + // MARK: - Scene var body: some Scene { WindowGroup { ContentView() - .environment(\.managedObjectContext, - persistenceManager.container.viewContext) + .environment(\.managedObjectContext, coreDataStack.container.viewContext) + .environmentObject(coreDataStack) .preferredColorScheme(colorSchemeMode.colorScheme) } .onChange(of: scenePhase) { _ in // save if: (1) app moved to background, and (2) changes are pending do { - try persistenceManager.container.viewContext.save() + try coreDataStack.container.viewContext.save() } catch { logger.error("\(#function): \(error.localizedDescription)") } diff --git a/Sources/Views/ColorSettings.swift b/Sources/Views/ColorSettings.swift deleted file mode 100644 index f9cddff..0000000 --- a/Sources/Views/ColorSettings.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// 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/Views/ContentView.swift b/Sources/Views/ContentView.swift index 834457a..084d3c4 100644 --- a/Sources/Views/ContentView.swift +++ b/Sources/Views/ContentView.swift @@ -13,9 +13,11 @@ import SwiftUI import GroutLib import GroutUI +import TrackerUI struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext + @EnvironmentObject private var manager: CoreDataStack enum Tabs: Int { case routines = 0 @@ -33,18 +35,15 @@ struct ContentView: View { var body: some View { TabView(selection: $selectedTab) { - NavStack(name: "routines", - navData: $routinesNavData) { - RoutineList(beforeStart: beforeStartAction) + NavStack(navData: $routinesNavData, destination: destination) { + RoutineList() } .tabItem { Label("Routines", systemImage: "dumbbell") } .tag(Tabs.routines.rawValue) - NavStack(name: "history", - navData: $historyNavData, - routineRunDetail: exerciseRunList) { + NavStack(navData: $historyNavData, destination: destination) { HistoryView() } .tabItem { @@ -52,8 +51,7 @@ struct ContentView: View { } .tag(Tabs.history.rawValue) - NavStack(name: "settings", - navData: $settingsNavData) { + NavStack(navData: $settingsNavData, destination: destination) { PhoneSettingsForm() } .tabItem { @@ -61,43 +59,50 @@ struct ContentView: View { } .tag(Tabs.settings.rawValue) } + .onContinueUserActivity(startRoutineActivityType) { + selectedTab = Tabs.routines.rawValue + handleStartRoutineUA(viewContext, $0) + } + } + + // handle routes for iOS-specific views here + @ViewBuilder + private func destination(_ router: GroutRouter, _ route: GroutRoute) -> some View { + switch route { + case let .exerciseRunList(routineRunUri): + exerciseRunList(routineRunUri) + .environmentObject(router) + .environment(\.managedObjectContext, viewContext) + default: + GroutDestination(route) + .environmentObject(router) + .environment(\.managedObjectContext, viewContext) + } } // used to inject view into NavStack @ViewBuilder private func exerciseRunList(_ routineRunUri: URL) -> some View { - if let zRoutineRun = ZRoutineRun.get(viewContext, forURIRepresentation: routineRunUri), - let archiveStore = PersistenceManager.getArchiveStore(viewContext) + if let zRoutineRun: ZRoutineRun = ZRoutineRun.get(viewContext, forURIRepresentation: routineRunUri), + let archiveStore = manager.getArchiveStore(viewContext) { ExerciseRunList(zRoutineRun: zRoutineRun, archiveStore: archiveStore) } else { Text("Routine Run not available to display detail.") } } - - // MARK: - Actions - - private func beforeStartAction() { - // in case routine is started via shortcut, force the first tab - - // NOTE: trying an explicit time delay, as it didn't switch the first time I tested. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - selectedTab = Tabs.routines.rawValue - } - } } struct ContentView_Previews: PreviewProvider { static var previews: some View { - let ctx = PersistenceManager.getPreviewContainer().viewContext + let manager = CoreDataStack.getPreviewStack() + let ctx = manager.container.viewContext let routine = Routine.create(ctx, userOrder: 0) routine.name = "Back & Bicep" - let e1 = Exercise.create(ctx, userOrder: 0) + let e1 = Exercise.create(ctx, routine: routine, userOrder: 0) e1.name = "Lat Pulldown" - e1.routine = routine - let e2 = Exercise.create(ctx, userOrder: 1) + let e2 = Exercise.create(ctx, routine: routine, userOrder: 1) e2.name = "Arm Curl" - e2.routine = routine return ContentView() .environment(\.managedObjectContext, ctx) } diff --git a/Sources/Views/ExerciseRunList.swift b/Sources/Views/ExerciseRunList.swift index 808278a..dd1716b 100644 --- a/Sources/Views/ExerciseRunList.swift +++ b/Sources/Views/ExerciseRunList.swift @@ -17,6 +17,8 @@ import Tabler import GroutLib import GroutUI +import TrackerLib +import TrackerUI struct ExerciseRunList: View { @Environment(\.verticalSizeClass) private var verticalSizeClass @@ -35,7 +37,7 @@ struct ExerciseRunList: View { self.zRoutineRun = zRoutineRun self.archiveStore = archiveStore - let predicate = NSPredicate(format: "zRoutineRun = %@", zRoutineRun) + let predicate = NSPredicate(format: "zRoutineRun == %@ AND userRemoved == %@", zRoutineRun, NSNumber(value: false)) let sortDescriptors = [NSSortDescriptor(keyPath: \ZExerciseRun.completedAt, ascending: true)] let request = makeRequest(ZExerciseRun.self, predicate: predicate, @@ -61,7 +63,7 @@ struct ExerciseRunList: View { private var listConfig: TablerListConfig { TablerListConfig( - onDelete: deleteAction + onDelete: userRemoveAction ) } @@ -189,19 +191,17 @@ struct ExerciseRunList: View { // MARK: - Actions - private func deleteAction(at offsets: IndexSet) { - // NOTE: removing specified zExerciseRun records, where present, from both mainStore and archiveStore. - + // NOTE: 'removes' matching records, where present, from both mainStore and archiveStore. + private func userRemoveAction(at offsets: IndexSet) { do { for index in offsets { let zExerciseRun = exerciseRuns[index] - guard let zExercise = zExerciseRun.zExercise, - let exerciseArchiveID = zExercise.exerciseArchiveID, + guard let exerciseArchiveID = zExerciseRun.zExercise?.exerciseArchiveID, let completedAt = zExerciseRun.completedAt else { continue } - try ZExerciseRun.delete(viewContext, exerciseArchiveID: exerciseArchiveID, completedAt: completedAt, inStore: nil) + try ZExerciseRun.userRemove(viewContext, exerciseArchiveID: exerciseArchiveID, completedAt: completedAt) } try viewContext.save() @@ -228,24 +228,31 @@ struct ExerciseRunList: View { struct ExerciseRunList_Previews: PreviewProvider { static var previews: some View { - let ctx = PersistenceManager.getPreviewContainer().viewContext - let archiveStore = PersistenceManager.getArchiveStore(ctx)! + let manager = CoreDataStack.getPreviewStack() + let ctx = manager.container.viewContext + let archiveStore = manager.getArchiveStore(ctx)! let routineArchiveID = UUID() let startedAt1 = Date.now.addingTimeInterval(-20000) let duration1 = 500.0 - let zR = ZRoutine.create(ctx, routineName: "blah", routineArchiveID: routineArchiveID, toStore: archiveStore) + let zR = ZRoutine.create(ctx, routineArchiveID: routineArchiveID, routineName: "blah", toStore: archiveStore) let zRR = ZRoutineRun.create(ctx, zRoutine: zR, startedAt: startedAt1, duration: duration1, toStore: archiveStore) let exerciseArchiveID1 = UUID() let exerciseArchiveID2 = UUID() + let exerciseArchiveID3 = UUID() let completedAt1 = startedAt1.addingTimeInterval(116) let completedAt2 = completedAt1.addingTimeInterval(173) + let completedAt3 = completedAt1.addingTimeInterval(210) let intensity1: Float = 150.0 let intensity2: Float = 200.0 - let zE1 = ZExercise.create(ctx, zRoutine: zR, exerciseName: "Lat Pulldown", exerciseUnits: .kilograms, exerciseArchiveID: exerciseArchiveID1, toStore: archiveStore) - let zE2 = ZExercise.create(ctx, zRoutine: zR, exerciseName: "Rear Delt", exerciseUnits: .none, exerciseArchiveID: exerciseArchiveID2, toStore: archiveStore) + let intensity3: Float = 50.0 + let zE1 = ZExercise.create(ctx, zRoutine: zR, exerciseArchiveID: exerciseArchiveID1, exerciseName: "Lat Pulldown", exerciseUnits: .kilograms, toStore: archiveStore) + let zE2 = ZExercise.create(ctx, zRoutine: zR, exerciseArchiveID: exerciseArchiveID2, exerciseName: "Rear Delt", exerciseUnits: .none, toStore: archiveStore) + let zE3 = ZExercise.create(ctx, zRoutine: zR, exerciseArchiveID: exerciseArchiveID3, exerciseName: "Arm Curl", exerciseUnits: .none, toStore: archiveStore) _ = ZExerciseRun.create(ctx, zRoutineRun: zRR, zExercise: zE1, completedAt: completedAt1, intensity: intensity1, toStore: archiveStore) - _ = ZExerciseRun.create(ctx, zRoutineRun: zRR, zExercise: zE2, completedAt: completedAt2, intensity: intensity2, toStore: archiveStore) + let er2 = ZExerciseRun.create(ctx, zRoutineRun: zRR, zExercise: zE2, completedAt: completedAt2, intensity: intensity2, toStore: archiveStore) + _ = ZExerciseRun.create(ctx, zRoutineRun: zRR, zExercise: zE3, completedAt: completedAt3, intensity: intensity3, toStore: archiveStore) + er2.userRemoved = true try! ctx.save() return NavigationStack { diff --git a/Sources/Views/ExportSettings.swift b/Sources/Views/ExportSettings.swift deleted file mode 100644 index 5e4d75d..0000000 --- a/Sources/Views/ExportSettings.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// 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(exportFormatKey) 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/Views/HistoryView.swift b/Sources/Views/HistoryView.swift index a16413a..1735263 100644 --- a/Sources/Views/HistoryView.swift +++ b/Sources/Views/HistoryView.swift @@ -14,16 +14,18 @@ import SwiftUI import GroutLib import GroutUI +import TrackerUI struct HistoryView: View { @Environment(\.managedObjectContext) private var viewContext - @EnvironmentObject private var router: MyRouter + @EnvironmentObject private var manager: CoreDataStack + @EnvironmentObject private var router: GroutRouter // MARK: - Parameters // MARK: - Locals - @State private var showAlert = false + @State private var showClearDialog = false private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: HistoryView.self)) @@ -31,27 +33,31 @@ struct HistoryView: View { // MARK: - Views var body: some View { - RoutineRunList(archiveStore: archiveStore) - .toolbar { - ToolbarItem(placement: .destructiveAction) { - Button(action: { - Haptics.play(.warning) - showAlert = true - }) { - Text("Clear") + if let archiveStore = manager.getArchiveStore(viewContext) { + RoutineRunList(archiveStore: archiveStore) + .toolbar { + ToolbarItem(placement: .destructiveAction) { + Button(action: { + Haptics.play(.warning) + showClearDialog = true + }) { + Text("Clear") + } } } - } - .alert("Are you sure?", - isPresented: $showAlert, - actions: { - Button("Delete", role: .destructive, action: clearHistoryAction) - }, - message: { - Text("This will remove all historical data.") - }) - .navigationTitle(navigationTitle) - .task(priority: .userInitiated, taskAction) + .confirmationDialog("", + isPresented: $showClearDialog, + actions: { + Button("Clear", role: .destructive, action: clearHistoryAction) + }, + message: { + Text("This will remove all historical data.") + }) + .navigationTitle(navigationTitle) + .task(priority: .userInitiated, taskAction) + } else { + Text("History not available.") + } } // MARK: - Properties @@ -60,20 +66,12 @@ struct HistoryView: View { "History" } - private var archiveStore: NSPersistentStore { - guard let store = PersistenceManager.getArchiveStore(viewContext) - else { - fatalError("unable to resolve archive store") - } - return store - } - // MARK: - Actions private func clearHistoryAction() { do { // clear all 'z' records from both mainStore and archiveStore - try PersistenceManager.clearZEntities(viewContext) + try manager.clearZEntities(viewContext) try viewContext.save() } catch { logger.error("\(#function): \(error.localizedDescription)") @@ -86,9 +84,18 @@ struct HistoryView: View { // transfer any 'Z' records from the 'Main' store to the 'Archive' store. - await PersistenceManager.shared.container.performBackgroundTask { backgroundContext in + await manager.container.performBackgroundTask { backgroundContext in + guard let mainStore = manager.getMainStore(backgroundContext), + let archiveStore = manager.getArchiveStore(backgroundContext) + else { + logger.error("\(#function): unable to acquire configuration to transfer log records.") + return + } + do { - try transferToArchive(backgroundContext) + try transferToArchive(backgroundContext, + mainStore: mainStore, + archiveStore: archiveStore) try backgroundContext.save() } catch { logger.error("\(#function): TRANSFER \(error.localizedDescription)") @@ -100,15 +107,16 @@ struct HistoryView: View { struct HistoryView_Previews: PreviewProvider { static var previews: some View { - let ctx = PersistenceManager.getPreviewContainer().viewContext - let archiveStore = PersistenceManager.getArchiveStore(ctx)! + let manager = CoreDataStack.getPreviewStack() + let ctx = manager.container.viewContext + let archiveStore = manager.getArchiveStore(ctx)! let routineArchiveID = UUID() let startedAt1 = Date.now.addingTimeInterval(-20000) let duration1 = 500.0 let startedAt2 = Date.now.addingTimeInterval(-10000) let duration2 = 400.0 - let zR = ZRoutine.create(ctx, routineName: "blah", routineArchiveID: routineArchiveID, toStore: archiveStore) + let zR = ZRoutine.create(ctx, routineArchiveID: routineArchiveID, routineName: "blah", toStore: archiveStore) _ = ZRoutineRun.create(ctx, zRoutine: zR, startedAt: startedAt1, duration: duration1, toStore: archiveStore) _ = ZRoutineRun.create(ctx, zRoutine: zR, startedAt: startedAt2, duration: duration2, toStore: archiveStore) try! ctx.save() diff --git a/Sources/Views/PhoneSettingsForm.swift b/Sources/Views/PhoneSettingsForm.swift index 6846b34..db8ac76 100644 --- a/Sources/Views/PhoneSettingsForm.swift +++ b/Sources/Views/PhoneSettingsForm.swift @@ -13,23 +13,35 @@ import SwiftUI import GroutLib import GroutUI +import TrackerLib +import TrackerUI struct PhoneSettingsForm: View { - @EnvironmentObject private var router: MyRouter + @Environment(\.managedObjectContext) private var viewContext + @EnvironmentObject private var manager: CoreDataStack + @EnvironmentObject private var router: GroutRouter // MARK: - Views var body: some View { - SettingsForm { - ColorSettings() - - ExportSettings() - - Button(action: { - router.path.append(MyRoutes.about) - }) { - Text("About \(appName)") + if let appSetting = try? AppSetting.getOrCreate(viewContext), + let mainStore = manager.getMainStore(viewContext), + let archiveStore = manager.getArchiveStore(viewContext) + { + GroutSettings(appSetting: appSetting, onRestoreToDefaults: {}) { + ExportSettings(mainStore: mainStore, + archiveStore: archiveStore, + filePrefix: "grt-", + createZipArchive: groutCreateZipArchive) + + Button(action: { + router.path.append(GroutRoute.about) + }) { + Text("About \(appName)") + } } + } else { + Text("Settings not available.") } } diff --git a/Sources/Views/RoutineRunList.swift b/Sources/Views/RoutineRunList.swift index 619432b..fee353d 100644 --- a/Sources/Views/RoutineRunList.swift +++ b/Sources/Views/RoutineRunList.swift @@ -10,21 +10,23 @@ import CoreData import os -import SwiftUI import StoreKit +import SwiftUI import Compactor import Tabler import GroutLib import GroutUI +import TrackerLib +import TrackerUI struct RoutineRunList: View { @Environment(\.requestReview) private var requestReview @Environment(\.verticalSizeClass) private var verticalSizeClass @Environment(\.colorScheme) private var colorScheme @Environment(\.managedObjectContext) private var viewContext - @EnvironmentObject private var router: MyRouter + @EnvironmentObject private var router: GroutRouter typealias Sort = TablerSort typealias Context = TablerContext @@ -37,8 +39,10 @@ struct RoutineRunList: View { internal init(archiveStore: NSPersistentStore) { self.archiveStore = archiveStore + let predicate = NSPredicate(format: "userRemoved == %@", NSNumber(value: false)) let sortDescriptors = [NSSortDescriptor(keyPath: \ZRoutineRun.startedAt, ascending: false)] let request = makeRequest(ZRoutineRun.self, + predicate: predicate, sortDescriptors: sortDescriptors, inStore: archiveStore) _routineRuns = FetchRequest(fetchRequest: request) @@ -67,7 +71,7 @@ struct RoutineRunList: View { private var listConfig: TablerListConfig { TablerListConfig( - onDelete: deleteAction + onDelete: userRemoveAction ) } @@ -81,7 +85,7 @@ struct RoutineRunList: View { // support for app review prompt @SceneStorage("has-been-prompted-for-app-review") private var hasBeenPromptedForAppReview = false - private let minimumRunsForAppReviewAlert = 15 + private let minimumRunsForAppReviewAlert = 30 // MARK: - Views @@ -118,7 +122,6 @@ struct RoutineRunList: View { .padding(columnPadding) durationText(element.duration) .lineLimit(1) -// ElapsedTimeText(elapsedSecs: element.duration) .padding(columnPadding) } .frame(maxWidth: .infinity) @@ -151,22 +154,19 @@ struct RoutineRunList: View { } private func detailAction(zRoutineRun: ZRoutineRun) { - router.path.append(MyRoutes.routineRunDetail(zRoutineRun.uriRepresentation)) + router.path.append(GroutRoute.exerciseRunList(zRoutineRun.uriRepresentation)) } - private func deleteAction(at offsets: IndexSet) { - // NOTE: removing specified zRoutineRun records, where present, from both mainStore and archiveStore. - + // NOTE: 'removes' matching records, where present, from both mainStore and archiveStore. + private func userRemoveAction(at offsets: IndexSet) { do { for index in offsets { let zRoutineRun = routineRuns[index] - - guard let zRoutine = zRoutineRun.zRoutine, - let routineArchiveID = zRoutine.routineArchiveID, + guard let routineArchiveID = zRoutineRun.zRoutine?.routineArchiveID, let startedAt = zRoutineRun.startedAt else { continue } - try ZRoutineRun.delete(viewContext, routineArchiveID: routineArchiveID, startedAt: startedAt, inStore: nil) + try ZRoutineRun.userRemove(viewContext, routineArchiveID: routineArchiveID, startedAt: startedAt) } try viewContext.save() @@ -178,15 +178,16 @@ struct RoutineRunList: View { struct RoutineRunList_Previews: PreviewProvider { static var previews: some View { - let ctx = PersistenceManager.getPreviewContainer().viewContext - let archiveStore = PersistenceManager.getArchiveStore(ctx)! + let manager = CoreDataStack.getPreviewStack() + let ctx = manager.container.viewContext + let archiveStore = manager.getArchiveStore(ctx)! let routineArchiveID = UUID() let startedAt1 = Date.now.addingTimeInterval(-20000) let duration1 = 500.0 let startedAt2 = Date.now.addingTimeInterval(-10000) let duration2 = 400.0 - let zR = ZRoutine.create(ctx, routineName: "Chest & Shoulder", routineArchiveID: routineArchiveID, toStore: archiveStore) + let zR = ZRoutine.create(ctx, routineArchiveID: routineArchiveID, routineName: "Chest & Shoulder", toStore: archiveStore) _ = ZRoutineRun.create(ctx, zRoutine: zR, startedAt: startedAt1, duration: duration1, toStore: archiveStore) _ = ZRoutineRun.create(ctx, zRoutine: zR, startedAt: startedAt2, duration: duration2, toStore: archiveStore) try! ctx.save()