From 320f3417dbf3a0cca694438b44ee5786ba2169df Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 13 Dec 2022 21:35:52 -0600 Subject: [PATCH 01/18] add Core Data base --- brain-marks.xcodeproj/project.pbxproj | 33 +++++++++++++++++++ .../BrainMarks.xcdatamodel/contents | 2 ++ brain-marks/CoreData/StorageProvider.swift | 26 +++++++++++++++ brain-marks/TelemetrySignals.swift | 3 ++ 4 files changed, 64 insertions(+) create mode 100644 brain-marks/CoreData/BrainMarks.xcdatamodeld/BrainMarks.xcdatamodel/contents create mode 100644 brain-marks/CoreData/StorageProvider.swift diff --git a/brain-marks.xcodeproj/project.pbxproj b/brain-marks.xcodeproj/project.pbxproj index ab69078..3c53049 100644 --- a/brain-marks.xcodeproj/project.pbxproj +++ b/brain-marks.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 20636BDDDBEF4EAE9CCE9D9E /* amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = ECA1CF04DB754255822691F2 /* amplifyconfiguration.json */; }; 684E2F2D3FCC4D36AC68E57D /* AWSCategory+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551BA45F30D640118D2F0CE2 /* AWSCategory+Schema.swift */; }; + 6961687B2949756D00D68070 /* BrainMarks.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 696168792949756D00D68070 /* BrainMarks.xcdatamodeld */; }; + 6961687D2949759E00D68070 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6961687C2949759E00D68070 /* CoreDataManager.swift */; }; + 6961687F294976A600D68070 /* StorageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6961687E294976A600D68070 /* StorageProvider.swift */; }; 91739DEB2622D2A7000F982A /* AddURLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91739DEA2622D2A7000F982A /* AddURLView.swift */; }; 91C8958926228D1500689196 /* Tweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C8958826228D1500689196 /* Tweet.swift */; }; 9C9314E5631F4C39AA06BF35 /* awsconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = DA7D79B731BC4ECFAAE8E817 /* awsconfiguration.json */; }; @@ -72,6 +75,9 @@ 1E886624D4EE4F64B9160A75 /* amplifytools.xcconfig */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.xcconfig; path = amplifytools.xcconfig; sourceTree = ""; }; 551BA45F30D640118D2F0CE2 /* AWSCategory+Schema.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "AWSCategory+Schema.swift"; path = "amplify/generated/models/AWSCategory+Schema.swift"; sourceTree = ""; }; 63D0518628EF45A382F08352 /* AWSCategory.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = AWSCategory.swift; path = amplify/generated/models/AWSCategory.swift; sourceTree = ""; }; + 6961687A2949756D00D68070 /* BrainMarks.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = BrainMarks.xcdatamodel; sourceTree = ""; }; + 6961687C2949759E00D68070 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; + 6961687E294976A600D68070 /* StorageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageProvider.swift; sourceTree = ""; }; 91739DEA2622D2A7000F982A /* AddURLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddURLView.swift; sourceTree = ""; }; 91C8958826228D1500689196 /* Tweet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tweet.swift; sourceTree = ""; }; A205CD412622A3EB00517DB5 /* TweetList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TweetList.swift; sourceTree = ""; }; @@ -155,6 +161,15 @@ path = .; sourceTree = ""; }; + 69616871294973F500D68070 /* CoreData */ = { + isa = PBXGroup; + children = ( + 696168792949756D00D68070 /* BrainMarks.xcdatamodeld */, + 6961687E294976A600D68070 /* StorageProvider.swift */, + ); + path = CoreData; + sourceTree = ""; + }; 6D7FDBA2D37342B39C7F1C69 /* AmplifyConfig */ = { isa = PBXGroup; children = ( @@ -193,6 +208,7 @@ children = ( FF5990682622DD61004DF328 /* DataStoreManager.swift */, FF554148282E9609002AEBED /* JSONDataManager.swift */, + 6961687C2949759E00D68070 /* CoreDataManager.swift */, ); path = Managers; sourceTree = ""; @@ -279,6 +295,7 @@ isa = PBXGroup; children = ( FF39430E262E863000A3623B /* AmplifyModelExtensions */, + 69616871294973F500D68070 /* CoreData */, FF39430C262E860E00A3623B /* Managers */, 91C8958726228CF600689196 /* Models */, FF39432A262FD05700A3623B /* Categories */, @@ -485,9 +502,11 @@ buildActionMask = 2147483647; files = ( 91C8958926228D1500689196 /* Tweet.swift in Sources */, + 6961687F294976A600D68070 /* StorageProvider.swift in Sources */, FFEBBB3C26223F75000F475F /* ContentView.swift in Sources */, FF554149282E9609002AEBED /* JSONDataManager.swift in Sources */, A224A9E62622BCED00AC12AF /* TweetCard.swift in Sources */, + 6961687B2949756D00D68070 /* BrainMarks.xcdatamodeld in Sources */, FFCB1097263E31D400544309 /* ReturnedTweet.swift in Sources */, FFC4CA7726867B4C00427C81 /* BMButton.swift in Sources */, FF21986F269FE57D00FFB406 /* AsyncImage.swift in Sources */, @@ -497,6 +516,7 @@ A2F449912622829D00725FEA /* CategoryList.swift in Sources */, A2F4498C2622802B00725FEA /* CategoryRow.swift in Sources */, FF5990692622DD61004DF328 /* DataStoreManager.swift in Sources */, + 6961687D2949759E00D68070 /* CoreDataManager.swift in Sources */, FFCB1099263E3E2D00544309 /* CategorySheetView.swift in Sources */, FF394316262FCF3900A3623B /* User.swift in Sources */, FFEBBB3A26223F75000F475F /* brain_marksApp.swift in Sources */, @@ -879,6 +899,19 @@ productName = TelemetryClient; }; /* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + 696168792949756D00D68070 /* BrainMarks.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 6961687A2949756D00D68070 /* BrainMarks.xcdatamodel */, + ); + currentVersion = 6961687A2949756D00D68070 /* BrainMarks.xcdatamodel */; + path = BrainMarks.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = FFEBBB2E26223F75000F475F /* Project object */; } diff --git a/brain-marks/CoreData/BrainMarks.xcdatamodeld/BrainMarks.xcdatamodel/contents b/brain-marks/CoreData/BrainMarks.xcdatamodeld/BrainMarks.xcdatamodel/contents new file mode 100644 index 0000000..e8dc614 --- /dev/null +++ b/brain-marks/CoreData/BrainMarks.xcdatamodeld/BrainMarks.xcdatamodel/contents @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/brain-marks/CoreData/StorageProvider.swift b/brain-marks/CoreData/StorageProvider.swift new file mode 100644 index 0000000..7354dae --- /dev/null +++ b/brain-marks/CoreData/StorageProvider.swift @@ -0,0 +1,26 @@ +// +// StorageProvider.swift +// brain-marks +// +// Created by Jay on 12/13/22. +// + +import Foundation +import CoreData +import TelemetryClient + +class StorageProvider { + let persistentContainer: NSPersistentContainer + + init() { + persistentContainer = NSPersistentContainer(name: "BrainMarks") + + persistentContainer.loadPersistentStores { description, error in + if let error = error { + TelemetryManager.send(TelemetrySignals.errorCoreDataLoad) + fatalError("Core Data store failed to load with error: \(error)") + } + print("Loaded Core Data \(description)") + } + } +} diff --git a/brain-marks/TelemetrySignals.swift b/brain-marks/TelemetrySignals.swift index 6f88e51..9fa4954 100644 --- a/brain-marks/TelemetrySignals.swift +++ b/brain-marks/TelemetrySignals.swift @@ -18,4 +18,7 @@ enum TelemetrySignals { /// The user added a new tweet. static let addTweet = "addTweet" + + /// Core Data failed to load + static let errorCoreDataLoad = "errorCoreDataLoad" } From 5248b1530cf3c51e4e3a031f09b4a227011714b9 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 29 Dec 2022 17:16:09 -0600 Subject: [PATCH 02/18] feat: migration should work --- brain-marks.xcodeproj/project.pbxproj | 20 +++++ brain-marks/ContentView.swift | 13 ++- .../BrainMarks.xcdatamodel/contents | 22 ++++- .../CoreData/StorageProvider+Tweet.swift | 31 +++++++ brain-marks/CoreData/StorageProvider.swift | 14 ++-- .../CoreData/StoreageProvider+Category.swift | 32 +++++++ brain-marks/Managers/CoreDataManager.swift | 27 ++++++ brain-marks/Services/MigrationService.swift | 84 +++++++++++++++++++ brain-marks/brain_marksApp.swift | 3 +- 9 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 brain-marks/CoreData/StorageProvider+Tweet.swift create mode 100644 brain-marks/CoreData/StoreageProvider+Category.swift create mode 100644 brain-marks/Managers/CoreDataManager.swift create mode 100644 brain-marks/Services/MigrationService.swift diff --git a/brain-marks.xcodeproj/project.pbxproj b/brain-marks.xcodeproj/project.pbxproj index 3c53049..c6fd472 100644 --- a/brain-marks.xcodeproj/project.pbxproj +++ b/brain-marks.xcodeproj/project.pbxproj @@ -12,6 +12,9 @@ 6961687B2949756D00D68070 /* BrainMarks.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 696168792949756D00D68070 /* BrainMarks.xcdatamodeld */; }; 6961687D2949759E00D68070 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6961687C2949759E00D68070 /* CoreDataManager.swift */; }; 6961687F294976A600D68070 /* StorageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6961687E294976A600D68070 /* StorageProvider.swift */; }; + 696168812949856100D68070 /* StoreageProvider+Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696168802949856100D68070 /* StoreageProvider+Category.swift */; }; + 69616883294988E700D68070 /* StorageProvider+Tweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69616882294988E700D68070 /* StorageProvider+Tweet.swift */; }; + 6961688629498D0600D68070 /* MigrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6961688529498D0600D68070 /* MigrationService.swift */; }; 91739DEB2622D2A7000F982A /* AddURLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91739DEA2622D2A7000F982A /* AddURLView.swift */; }; 91C8958926228D1500689196 /* Tweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C8958826228D1500689196 /* Tweet.swift */; }; 9C9314E5631F4C39AA06BF35 /* awsconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = DA7D79B731BC4ECFAAE8E817 /* awsconfiguration.json */; }; @@ -78,6 +81,9 @@ 6961687A2949756D00D68070 /* BrainMarks.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = BrainMarks.xcdatamodel; sourceTree = ""; }; 6961687C2949759E00D68070 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; 6961687E294976A600D68070 /* StorageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageProvider.swift; sourceTree = ""; }; + 696168802949856100D68070 /* StoreageProvider+Category.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoreageProvider+Category.swift"; sourceTree = ""; }; + 69616882294988E700D68070 /* StorageProvider+Tweet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageProvider+Tweet.swift"; sourceTree = ""; }; + 6961688529498D0600D68070 /* MigrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationService.swift; sourceTree = ""; }; 91739DEA2622D2A7000F982A /* AddURLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddURLView.swift; sourceTree = ""; }; 91C8958826228D1500689196 /* Tweet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tweet.swift; sourceTree = ""; }; A205CD412622A3EB00517DB5 /* TweetList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TweetList.swift; sourceTree = ""; }; @@ -166,10 +172,20 @@ children = ( 696168792949756D00D68070 /* BrainMarks.xcdatamodeld */, 6961687E294976A600D68070 /* StorageProvider.swift */, + 69616882294988E700D68070 /* StorageProvider+Tweet.swift */, + 696168802949856100D68070 /* StoreageProvider+Category.swift */, ); path = CoreData; sourceTree = ""; }; + 6961688429498CF100D68070 /* Services */ = { + isa = PBXGroup; + children = ( + 6961688529498D0600D68070 /* MigrationService.swift */, + ); + path = Services; + sourceTree = ""; + }; 6D7FDBA2D37342B39C7F1C69 /* AmplifyConfig */ = { isa = PBXGroup; children = ( @@ -297,6 +313,7 @@ FF39430E262E863000A3623B /* AmplifyModelExtensions */, 69616871294973F500D68070 /* CoreData */, FF39430C262E860E00A3623B /* Managers */, + 6961688429498CF100D68070 /* Services */, 91C8958726228CF600689196 /* Models */, FF39432A262FD05700A3623B /* Categories */, FF39432D262FD0AD00A3623B /* Tweets */, @@ -509,6 +526,7 @@ 6961687B2949756D00D68070 /* BrainMarks.xcdatamodeld in Sources */, FFCB1097263E31D400544309 /* ReturnedTweet.swift in Sources */, FFC4CA7726867B4C00427C81 /* BMButton.swift in Sources */, + 69616883294988E700D68070 /* StorageProvider+Tweet.swift in Sources */, FF21986F269FE57D00FFB406 /* AsyncImage.swift in Sources */, FFEB1B5826A9F9C300682C37 /* Alert.swift in Sources */, FF36B6712623678D007A6D7F /* AWSTweet+Extension.swift in Sources */, @@ -516,6 +534,7 @@ A2F449912622829D00725FEA /* CategoryList.swift in Sources */, A2F4498C2622802B00725FEA /* CategoryRow.swift in Sources */, FF5990692622DD61004DF328 /* DataStoreManager.swift in Sources */, + 6961688629498D0600D68070 /* MigrationService.swift in Sources */, 6961687D2949759E00D68070 /* CoreDataManager.swift in Sources */, FFCB1099263E3E2D00544309 /* CategorySheetView.swift in Sources */, FF394316262FCF3900A3623B /* User.swift in Sources */, @@ -527,6 +546,7 @@ CF16AFB9BE3F401E98C85114 /* AWSTweet.swift in Sources */, FF36B661262357F9007A6D7F /* AWSCategory+Extension.swift in Sources */, FFD5152E28F9FAA500665625 /* TelemetrySignals.swift in Sources */, + 696168812949856100D68070 /* StoreageProvider+Category.swift in Sources */, FF36B66C26235FE6007A6D7F /* TweetListViewModel.swift in Sources */, C4392E80BFE640DAA2625F9D /* AWSTweet+Schema.swift in Sources */, A205CD422622A3EB00517DB5 /* TweetList.swift in Sources */, diff --git a/brain-marks/ContentView.swift b/brain-marks/ContentView.swift index 19d6ad9..cf9cc72 100644 --- a/brain-marks/ContentView.swift +++ b/brain-marks/ContentView.swift @@ -8,15 +8,26 @@ import SwiftUI struct ContentView: View { + let migrationService: MigrationService + let storageProvider: StorageProvider + @State private var showAddSheet = false + init(storageProvider: StorageProvider) { + self.migrationService = MigrationService(managedObjectContext: storageProvider.context) + self.storageProvider = storageProvider + } + var body: some View { CategoryList() + .onAppear { + migrationService.performMigration() + } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView() + ContentView(storageProvider: StorageProvider()) } } diff --git a/brain-marks/CoreData/BrainMarks.xcdatamodeld/BrainMarks.xcdatamodel/contents b/brain-marks/CoreData/BrainMarks.xcdatamodeld/BrainMarks.xcdatamodel/contents index e8dc614..6a92aee 100644 --- a/brain-marks/CoreData/BrainMarks.xcdatamodeld/BrainMarks.xcdatamodel/contents +++ b/brain-marks/CoreData/BrainMarks.xcdatamodeld/BrainMarks.xcdatamodel/contents @@ -1,2 +1,22 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/brain-marks/CoreData/StorageProvider+Tweet.swift b/brain-marks/CoreData/StorageProvider+Tweet.swift new file mode 100644 index 0000000..7e0f164 --- /dev/null +++ b/brain-marks/CoreData/StorageProvider+Tweet.swift @@ -0,0 +1,31 @@ +// +// StorageProvider+Tweet.swift +// brain-marks +// +// Created by Jay on 12/13/22. +// + +import Foundation +import CoreData + +extension StorageProvider { + func saveTweet(_ entity: TweetEntity) { + context.insert(entity) + do { + try context.save() + } catch { + print("StorageProvider.saveTweet(:): Error \(error)") + } + } + + func getAllTweets() -> [TweetEntity] { + let fetchRequest: NSFetchRequest = TweetEntity.fetchRequest() + + do { + return try context.fetch(fetchRequest) + } catch { + print("StorageProvider.getAllTweets(:): Error \(error)") + return [] + } + } +} diff --git a/brain-marks/CoreData/StorageProvider.swift b/brain-marks/CoreData/StorageProvider.swift index 7354dae..8576c69 100644 --- a/brain-marks/CoreData/StorageProvider.swift +++ b/brain-marks/CoreData/StorageProvider.swift @@ -10,17 +10,21 @@ import CoreData import TelemetryClient class StorageProvider { - let persistentContainer: NSPersistentContainer + static var persistentContainer: NSPersistentContainer { + let container = NSPersistentContainer(name: "BrainMarks") - init() { - persistentContainer = NSPersistentContainer(name: "BrainMarks") - - persistentContainer.loadPersistentStores { description, error in + container.loadPersistentStores { description, error in if let error = error { TelemetryManager.send(TelemetrySignals.errorCoreDataLoad) fatalError("Core Data store failed to load with error: \(error)") } print("Loaded Core Data \(description)") } + + return container + } + + var context: NSManagedObjectContext { + return Self.persistentContainer.viewContext } } diff --git a/brain-marks/CoreData/StoreageProvider+Category.swift b/brain-marks/CoreData/StoreageProvider+Category.swift new file mode 100644 index 0000000..017539a --- /dev/null +++ b/brain-marks/CoreData/StoreageProvider+Category.swift @@ -0,0 +1,32 @@ +// +// StoreageProvider+Category.swift +// brain-marks +// +// Created by Jay on 12/13/22. +// + +import Foundation +import CoreData + +extension StorageProvider { + func saveCategory(_ entity: CategoryEntity) throws { + context.insert(entity) + do { + try context.save() + print("✅ StorageProvider.saveCategory(:): SUCCESS") + } catch { + print("❌ StorageProvider.saveCategory(:): ERROR \(error)") + } + } + + func getAllCategories() -> [CategoryEntity] { + let fetchRequest: NSFetchRequest = CategoryEntity.fetchRequest() + + do { + return try context.fetch(fetchRequest) + } catch { + print("StorageProvider.getAllCategories(:): Error \(error)") + return [] + } + } +} diff --git a/brain-marks/Managers/CoreDataManager.swift b/brain-marks/Managers/CoreDataManager.swift new file mode 100644 index 0000000..185d80d --- /dev/null +++ b/brain-marks/Managers/CoreDataManager.swift @@ -0,0 +1,27 @@ +// +// CoreDataManager.swift +// brain-marks +// +// Created by Jay on 12/13/22. +// + +import Foundation + +class CoreDataManager { + + static let shared = CoreDataManager() + + private var storageProvider: StorageProvider + + init() { + self.storageProvider = StorageProvider() + } + + func addCategory(_ category: CategoryEntity) { + do { + try storageProvider.saveCategory(category) + } catch { + print("CoreDataManager.addCategory() Error: \(error)") + } + } +} diff --git a/brain-marks/Services/MigrationService.swift b/brain-marks/Services/MigrationService.swift new file mode 100644 index 0000000..be6e7cd --- /dev/null +++ b/brain-marks/Services/MigrationService.swift @@ -0,0 +1,84 @@ +// +// MigrationService.swift +// brain-marks +// +// Created by Jay on 12/13/22. +// + +import CoreData +import Foundation + +/// Controls the migration from Amplify to CoreData +class MigrationService { + /// Need to get category from Amplify + /// Store Category inside CD + /// Loop through category and get tweets + /// Store tweet in CD and associate to proper category + private var awsCategories: [AWSCategory] = [] + + private let amplifyDataStore = DataStoreManger.shared + private let coreDataStore = CoreDataManager.shared + private var managedObjectContext: NSManagedObjectContext + + init(managedObjectContext: NSManagedObjectContext) { + self.managedObjectContext = managedObjectContext + } + + func performMigration() { + /// 1. Fetch categories + getCategories() + /// 2. Go through each category and get tweets + for awsCategory in awsCategories { + /// 3. Add category to core data + let categoryToAdd = makeCategoryCoreDataCompatible(category: awsCategory) + amplifyDataStore.fetchSavedTweets(for: awsCategory) { awsTweets in + for tweet in awsTweets ?? [] { + let tweetToAdd = self.makeTweetCoreDataCompatible(tweet: tweet) + categoryToAdd.addToTweets(tweetToAdd) + } + } + do { + try managedObjectContext.save() + } catch { + print("❌ MigrationService.performMigration() Error: \(error)") + } + } + + } + + private func getCategories() { + amplifyDataStore.fetchCategories(completion: { result in + switch result { + case .success(let categories): + self.awsCategories = categories + case .failure(let error): + print("MigrationService.getCategories(): Error: \(error)") + } + }) + } + + private func makeCategoryCoreDataCompatible(category: AWSCategory) -> CategoryEntity { + let tweetCat = CategoryEntity(context: managedObjectContext) + tweetCat.amplifyID = category.id + tweetCat.id = UUID() + tweetCat.dateCreated = Date() + tweetCat.dateModified = Date() + tweetCat.imageName = category.imageName ?? "folder" + tweetCat.name = category.name + + return tweetCat + } + + private func makeTweetCoreDataCompatible(tweet: AWSTweet) -> TweetEntity { + let tweetEntity = TweetEntity(context: managedObjectContext) + tweetEntity.id = UUID(uuidString: tweet.id) + tweetEntity.tweetID = tweet.tweetID + tweetEntity.authorName = tweet.authorName + tweetEntity.authorUsername = tweet.authorUsername + tweetEntity.dateCreated = Date() + tweetEntity.profileImageURL = tweet.profileImageURL + tweetEntity.text = tweet.text + + return tweetEntity + } +} diff --git a/brain-marks/brain_marksApp.swift b/brain-marks/brain_marksApp.swift index dffceb7..f64bca8 100644 --- a/brain-marks/brain_marksApp.swift +++ b/brain-marks/brain_marksApp.swift @@ -20,10 +20,11 @@ struct brain_marksApp: App { TelemetryManager.initialize(with: configuration) TelemetryManager.send(TelemetrySignals.appLaunchedRegularly) } + let storageProvider = StorageProvider() var body: some Scene { WindowGroup { - ContentView() + ContentView(storageProvider: storageProvider) } } From adfc4e2aa5464ada5372fd1a6fa7f9f82e032aba Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 29 Dec 2022 20:45:06 -0600 Subject: [PATCH 03/18] feat: categories work --- brain-marks.xcodeproj/project.pbxproj | 4 --- .../Categories/CategoryListViewModel.swift | 26 ++++++--------- .../Categories/Views/CategoryList.swift | 12 ++----- .../Categories/Views/CategoryRow.swift | 22 ++++++------- brain-marks/ContentView.swift | 2 -- brain-marks/CoreData/StorageProvider.swift | 32 ++++++++++++++++--- brain-marks/Managers/CoreDataManager.swift | 27 ---------------- brain-marks/Services/MigrationService.swift | 2 +- brain-marks/Tweets/Views/TweetList.swift | 6 ++-- brain-marks/brain_marksApp.swift | 5 +-- 10 files changed, 57 insertions(+), 81 deletions(-) delete mode 100644 brain-marks/Managers/CoreDataManager.swift diff --git a/brain-marks.xcodeproj/project.pbxproj b/brain-marks.xcodeproj/project.pbxproj index c6fd472..1729bbb 100644 --- a/brain-marks.xcodeproj/project.pbxproj +++ b/brain-marks.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 20636BDDDBEF4EAE9CCE9D9E /* amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = ECA1CF04DB754255822691F2 /* amplifyconfiguration.json */; }; 684E2F2D3FCC4D36AC68E57D /* AWSCategory+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551BA45F30D640118D2F0CE2 /* AWSCategory+Schema.swift */; }; 6961687B2949756D00D68070 /* BrainMarks.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 696168792949756D00D68070 /* BrainMarks.xcdatamodeld */; }; - 6961687D2949759E00D68070 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6961687C2949759E00D68070 /* CoreDataManager.swift */; }; 6961687F294976A600D68070 /* StorageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6961687E294976A600D68070 /* StorageProvider.swift */; }; 696168812949856100D68070 /* StoreageProvider+Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696168802949856100D68070 /* StoreageProvider+Category.swift */; }; 69616883294988E700D68070 /* StorageProvider+Tweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69616882294988E700D68070 /* StorageProvider+Tweet.swift */; }; @@ -79,7 +78,6 @@ 551BA45F30D640118D2F0CE2 /* AWSCategory+Schema.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "AWSCategory+Schema.swift"; path = "amplify/generated/models/AWSCategory+Schema.swift"; sourceTree = ""; }; 63D0518628EF45A382F08352 /* AWSCategory.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = AWSCategory.swift; path = amplify/generated/models/AWSCategory.swift; sourceTree = ""; }; 6961687A2949756D00D68070 /* BrainMarks.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = BrainMarks.xcdatamodel; sourceTree = ""; }; - 6961687C2949759E00D68070 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; 6961687E294976A600D68070 /* StorageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageProvider.swift; sourceTree = ""; }; 696168802949856100D68070 /* StoreageProvider+Category.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoreageProvider+Category.swift"; sourceTree = ""; }; 69616882294988E700D68070 /* StorageProvider+Tweet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageProvider+Tweet.swift"; sourceTree = ""; }; @@ -224,7 +222,6 @@ children = ( FF5990682622DD61004DF328 /* DataStoreManager.swift */, FF554148282E9609002AEBED /* JSONDataManager.swift */, - 6961687C2949759E00D68070 /* CoreDataManager.swift */, ); path = Managers; sourceTree = ""; @@ -535,7 +532,6 @@ A2F4498C2622802B00725FEA /* CategoryRow.swift in Sources */, FF5990692622DD61004DF328 /* DataStoreManager.swift in Sources */, 6961688629498D0600D68070 /* MigrationService.swift in Sources */, - 6961687D2949759E00D68070 /* CoreDataManager.swift in Sources */, FFCB1099263E3E2D00544309 /* CategorySheetView.swift in Sources */, FF394316262FCF3900A3623B /* User.swift in Sources */, FFEBBB3A26223F75000F475F /* brain_marksApp.swift in Sources */, diff --git a/brain-marks/Categories/CategoryListViewModel.swift b/brain-marks/Categories/CategoryListViewModel.swift index 4f5845a..311927f 100644 --- a/brain-marks/Categories/CategoryListViewModel.swift +++ b/brain-marks/Categories/CategoryListViewModel.swift @@ -8,28 +8,20 @@ import SwiftUI final class CategoryListViewModel: ObservableObject { - - @Published var categories = [AWSCategory]() + + let storageProvider: StorageProvider + + @Published var categories = [CategoryEntity]() + + init(inMemory: Bool = false) { + storageProvider = inMemory ? StorageProvider.preview : StorageProvider.shared + } func getCategories() { - categories = [] - DataStoreManger.shared.fetchCategories { result in - switch result { - case .success(let categories): - DispatchQueue.main.async { - self.categories = categories - } - case .failure(let error): - print("Error fetching categories: \(error)") - } - } + categories = storageProvider.getAllCategories() } func deleteCategory(at offsets: IndexSet) { - for offset in offsets { - let category = categories[offset] - DataStoreManger.shared.deleteCategory(category: category) - } categories.remove(atOffsets: offsets) } diff --git a/brain-marks/Categories/Views/CategoryList.swift b/brain-marks/Categories/Views/CategoryList.swift index f9e9675..099b16c 100644 --- a/brain-marks/Categories/Views/CategoryList.swift +++ b/brain-marks/Categories/Views/CategoryList.swift @@ -51,7 +51,7 @@ struct CategoryList: View { Image(systemName:"plus.circle") } .sheet(isPresented: $showAddURLView) { - AddURLView(categories: viewModel.categories) +// AddURLView(categories: viewModel.categories) } } } @@ -66,12 +66,6 @@ struct CategoryList: View { @ViewBuilder var categoryList: some View { categories - // removing for now, this makes the UI "flash" when updating a category -// if viewModel.categories.isEmpty { -// emptyListView -// } else { -// categories -// } } var emptyListView: some View { @@ -82,13 +76,13 @@ struct CategoryList: View { var categories: some View { List { - ForEach(viewModel.categories) { category in + ForEach(viewModel.categories, id: \.id) { category in NavigationLink(destination: TweetList(category: category)) { CategoryRow(category: category) } .contextMenu { Button { - editCategory = category +// editCategory = category categorySheetState = .edit showingCategorySheet.toggle() } label: { diff --git a/brain-marks/Categories/Views/CategoryRow.swift b/brain-marks/Categories/Views/CategoryRow.swift index a5781f4..f1c9215 100644 --- a/brain-marks/Categories/Views/CategoryRow.swift +++ b/brain-marks/Categories/Views/CategoryRow.swift @@ -8,7 +8,7 @@ import SwiftUI struct CategoryRow: View { - let category: AWSCategory + let category: CategoryEntity var body: some View { HStack { @@ -16,18 +16,18 @@ struct CategoryRow: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) - Text(category.name) + Text(category.name ?? "No name") Spacer() } } } -struct CategoryRow_Previews: PreviewProvider { - static var previews: some View { - Group { - CategoryRow(category: AWSCategory(id: "234", name: "SwiftUI", imageName: "swift")) - CategoryRow(category: AWSCategory(id: "987", name: "BigBrainHacks", imageName: "laptopcomputer")) - } - .previewLayout(.fixed(width: 300, height: 70)) - } -} +//struct CategoryRow_Previews: PreviewProvider { +// static var previews: some View { +// Group { +// CategoryRow(category: CategoryEntity(id: "234", name: "SwiftUI", imageName: "swift")) +// CategoryRow(category: CategoryEntity(id: "987", name: "BigBrainHacks", imageName: "laptopcomputer")) +// } +// .previewLayout(.fixed(width: 300, height: 70)) +// } +//} diff --git a/brain-marks/ContentView.swift b/brain-marks/ContentView.swift index cf9cc72..ef5b87e 100644 --- a/brain-marks/ContentView.swift +++ b/brain-marks/ContentView.swift @@ -9,13 +9,11 @@ import SwiftUI struct ContentView: View { let migrationService: MigrationService - let storageProvider: StorageProvider @State private var showAddSheet = false init(storageProvider: StorageProvider) { self.migrationService = MigrationService(managedObjectContext: storageProvider.context) - self.storageProvider = storageProvider } var body: some View { diff --git a/brain-marks/CoreData/StorageProvider.swift b/brain-marks/CoreData/StorageProvider.swift index 8576c69..7c25cb8 100644 --- a/brain-marks/CoreData/StorageProvider.swift +++ b/brain-marks/CoreData/StorageProvider.swift @@ -10,9 +10,33 @@ import CoreData import TelemetryClient class StorageProvider { - static var persistentContainer: NSPersistentContainer { - let container = NSPersistentContainer(name: "BrainMarks") + static let shared = StorageProvider() + + let container: NSPersistentContainer + + static var preview: StorageProvider = { + let controller = StorageProvider(inMemory: true) + for num in 0..<10 { + let category = CategoryEntity(context: controller.context) + category.name = "Category \(num)" + category.id = UUID() + category.dateCreated = Date() + category.dateModified = Date() + } + return controller + }() + + init(inMemory: Bool = false) { + container = NSPersistentContainer(name: "BrainMarks") + + if inMemory { + if #available(iOS 16.0, *) { + container.persistentStoreDescriptions.first?.url = URL(filePath: "/dev/null") + } else { + container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null") + } + } container.loadPersistentStores { description, error in if let error = error { TelemetryManager.send(TelemetrySignals.errorCoreDataLoad) @@ -20,11 +44,9 @@ class StorageProvider { } print("Loaded Core Data \(description)") } - - return container } var context: NSManagedObjectContext { - return Self.persistentContainer.viewContext + return container.viewContext } } diff --git a/brain-marks/Managers/CoreDataManager.swift b/brain-marks/Managers/CoreDataManager.swift deleted file mode 100644 index 185d80d..0000000 --- a/brain-marks/Managers/CoreDataManager.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// CoreDataManager.swift -// brain-marks -// -// Created by Jay on 12/13/22. -// - -import Foundation - -class CoreDataManager { - - static let shared = CoreDataManager() - - private var storageProvider: StorageProvider - - init() { - self.storageProvider = StorageProvider() - } - - func addCategory(_ category: CategoryEntity) { - do { - try storageProvider.saveCategory(category) - } catch { - print("CoreDataManager.addCategory() Error: \(error)") - } - } -} diff --git a/brain-marks/Services/MigrationService.swift b/brain-marks/Services/MigrationService.swift index be6e7cd..5debe5a 100644 --- a/brain-marks/Services/MigrationService.swift +++ b/brain-marks/Services/MigrationService.swift @@ -17,7 +17,7 @@ class MigrationService { private var awsCategories: [AWSCategory] = [] private let amplifyDataStore = DataStoreManger.shared - private let coreDataStore = CoreDataManager.shared + private var managedObjectContext: NSManagedObjectContext init(managedObjectContext: NSManagedObjectContext) { diff --git a/brain-marks/Tweets/Views/TweetList.swift b/brain-marks/Tweets/Views/TweetList.swift index 4b1e55f..b5e7b85 100644 --- a/brain-marks/Tweets/Views/TweetList.swift +++ b/brain-marks/Tweets/Views/TweetList.swift @@ -9,7 +9,7 @@ import SwiftUI struct TweetList: View { - let category: AWSCategory + let category: CategoryEntity @StateObject var viewModel = TweetListViewModel() @@ -23,12 +23,12 @@ struct TweetList: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 25, height: 25) - Text(category.name) + Text(category.name ?? "No name") } } } .onAppear { - viewModel.fetchTweets(category: category) +// viewModel.fetchTweets(category: category) } } diff --git a/brain-marks/brain_marksApp.swift b/brain-marks/brain_marksApp.swift index f64bca8..39ec075 100644 --- a/brain-marks/brain_marksApp.swift +++ b/brain-marks/brain_marksApp.swift @@ -20,8 +20,9 @@ struct brain_marksApp: App { TelemetryManager.initialize(with: configuration) TelemetryManager.send(TelemetrySignals.appLaunchedRegularly) } - let storageProvider = StorageProvider() - + + let storageProvider = StorageProvider.shared + var body: some Scene { WindowGroup { ContentView(storageProvider: storageProvider) From 5d944e63a527480b48346724cf5cab1f89f26c95 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 29 Dec 2022 20:55:06 -0600 Subject: [PATCH 04/18] feat: can view tweets from CD --- brain-marks/Tweets/TweetListViewModel.swift | 12 +++++------- brain-marks/Tweets/Views/TweetCard.swift | 4 ++-- brain-marks/Tweets/Views/TweetList.swift | 9 ++++++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/brain-marks/Tweets/TweetListViewModel.swift b/brain-marks/Tweets/TweetListViewModel.swift index d51f33e..9975794 100644 --- a/brain-marks/Tweets/TweetListViewModel.swift +++ b/brain-marks/Tweets/TweetListViewModel.swift @@ -9,13 +9,11 @@ import SwiftUI final class TweetListViewModel: ObservableObject { - @Published var tweets = [AWSTweet]() - - func fetchTweets(category: AWSCategory) { - DataStoreManger.shared.fetchSavedTweets(for: category) { tweets in - if let tweets = tweets { - self.tweets = tweets - } + @Published var tweets = [TweetEntity]() + + func setTweets(tweets: NSSet) { + for tweet in tweets { + self.tweets.append(tweet as! TweetEntity) } } diff --git a/brain-marks/Tweets/Views/TweetCard.swift b/brain-marks/Tweets/Views/TweetCard.swift index 494ab10..d52de7b 100644 --- a/brain-marks/Tweets/Views/TweetCard.swift +++ b/brain-marks/Tweets/Views/TweetCard.swift @@ -9,7 +9,7 @@ import SwiftUI struct TweetCard: View { - @State var tweet: AWSTweet + @State var tweet: TweetEntity var body: some View { VStack(alignment: .leading) { @@ -22,7 +22,7 @@ struct TweetCard: View { struct TweetHeaderView: View { - let tweet: AWSTweet + let tweet: TweetEntity var body: some View { HStack { diff --git a/brain-marks/Tweets/Views/TweetList.swift b/brain-marks/Tweets/Views/TweetList.swift index b5e7b85..7938cbd 100644 --- a/brain-marks/Tweets/Views/TweetList.swift +++ b/brain-marks/Tweets/Views/TweetList.swift @@ -28,7 +28,7 @@ struct TweetList: View { } } .onAppear { -// viewModel.fetchTweets(category: category) + viewModel.setTweets(tweets: category.tweets ?? NSSet()) } } @@ -49,10 +49,13 @@ struct TweetList: View { var tweets: some View { List { - ForEach(viewModel.tweets) { tweet in + ForEach(viewModel.tweets, id: \.id!) { tweet in TweetCard(tweet: tweet) .onTapGesture { - viewModel.openTwitter(tweetID: tweet.tweetID, authorUsername: tweet.authorUsername!) + viewModel.openTwitter( + tweetID: tweet.tweetID ?? "no tweet id", + authorUsername: tweet.authorUsername! + ) } } .onDelete { offsets in From 109b61b6e9e64147a073d20ec2cec4b1f75faa32 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 29 Dec 2022 21:29:36 -0600 Subject: [PATCH 05/18] allows previews to work --- .../Categories/CategoryListViewModel.swift | 19 ++++++++++++++-- .../Categories/Views/CategoryList.swift | 3 +-- .../Categories/Views/CategoryRow.swift | 22 +++++++++++-------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/brain-marks/Categories/CategoryListViewModel.swift b/brain-marks/Categories/CategoryListViewModel.swift index 311927f..1eb564a 100644 --- a/brain-marks/Categories/CategoryListViewModel.swift +++ b/brain-marks/Categories/CategoryListViewModel.swift @@ -13,8 +13,23 @@ final class CategoryListViewModel: ObservableObject { @Published var categories = [CategoryEntity]() - init(inMemory: Bool = false) { - storageProvider = inMemory ? StorageProvider.preview : StorageProvider.shared + init() { + /// Using a compiler statement here to determine where the code is being ran. + /// A compiler statement was chosen so that it doesn't interfere with run time + /// in production. + /// + /// If it's being ran in the canvas (preview) then we want to use the preview container + /// If it's being ran not in the canvas, then we want to use the CoreData file for existing data. + #if DEBUG + // Checks if the code is running in the cancas (preview) + if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { + storageProvider = .preview + } else { + storageProvider = .shared + } + #else + storageProvider = .shared + #endif } func getCategories() { diff --git a/brain-marks/Categories/Views/CategoryList.swift b/brain-marks/Categories/Views/CategoryList.swift index 099b16c..6df0d75 100644 --- a/brain-marks/Categories/Views/CategoryList.swift +++ b/brain-marks/Categories/Views/CategoryList.swift @@ -13,7 +13,6 @@ enum CategoryState { } struct CategoryList: View { - @State private var categorySheetState: CategoryState = .new @State private var editCategory: AWSCategory? @State private var indexSetToDelete: IndexSet? @@ -22,7 +21,7 @@ struct CategoryList: View { @State private var showingDeleteActionSheet = false @StateObject var viewModel = CategoryListViewModel() - + var body: some View { NavigationView { categoryList diff --git a/brain-marks/Categories/Views/CategoryRow.swift b/brain-marks/Categories/Views/CategoryRow.swift index f1c9215..0a4356d 100644 --- a/brain-marks/Categories/Views/CategoryRow.swift +++ b/brain-marks/Categories/Views/CategoryRow.swift @@ -22,12 +22,16 @@ struct CategoryRow: View { } } -//struct CategoryRow_Previews: PreviewProvider { -// static var previews: some View { -// Group { -// CategoryRow(category: CategoryEntity(id: "234", name: "SwiftUI", imageName: "swift")) -// CategoryRow(category: CategoryEntity(id: "987", name: "BigBrainHacks", imageName: "laptopcomputer")) -// } -// .previewLayout(.fixed(width: 300, height: 70)) -// } -//} +struct CategoryRow_Previews: PreviewProvider { + static var previews: some View { + Group { + CategoryRow( + category: StorageProvider.preview.getAllCategories()[0] + ) + CategoryRow( + category: StorageProvider.preview.getAllCategories()[1] + ) + } + .previewLayout(.fixed(width: 300, height: 70)) + } +} From ab5718895d8299df9c38366b3485fb5960001fd0 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 29 Dec 2022 21:45:29 -0600 Subject: [PATCH 06/18] fixes tweet card previews --- brain-marks/CoreData/StorageProvider.swift | 8 ++++++++ brain-marks/Tweets/Views/TweetCard.swift | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/brain-marks/CoreData/StorageProvider.swift b/brain-marks/CoreData/StorageProvider.swift index 7c25cb8..0d3931e 100644 --- a/brain-marks/CoreData/StorageProvider.swift +++ b/brain-marks/CoreData/StorageProvider.swift @@ -23,6 +23,14 @@ class StorageProvider { category.id = UUID() category.dateCreated = Date() category.dateModified = Date() + for tweetNum in 0..<5 { + let tweet = TweetEntity(context: controller.context) + tweet.id = UUID() + tweet.dateCreated = Date() + tweet.authorName = "heyjaywilson" + tweet.text = "this is a sample tweet \(tweetNum)" + category.addToTweets(tweet) + } } return controller }() diff --git a/brain-marks/Tweets/Views/TweetCard.swift b/brain-marks/Tweets/Views/TweetCard.swift index d52de7b..a2230af 100644 --- a/brain-marks/Tweets/Views/TweetCard.swift +++ b/brain-marks/Tweets/Views/TweetCard.swift @@ -14,7 +14,7 @@ struct TweetCard: View { var body: some View { VStack(alignment: .leading) { TweetHeaderView(tweet: tweet) - TweetBodyView(tweetBody: tweet.text!) + TweetBodyView(tweetBody: tweet.text ?? "help") // TweetFooterView() } } @@ -146,7 +146,7 @@ struct UserInfoView: View { struct TweetCard_Previews: PreviewProvider { static var previews: some View { - TweetCard(tweet: AWSTweet(id: "123", tweetID: "234", text: "Tweet ext here")) + TweetCard(tweet: StorageProvider.preview.getAllTweets().first!) .previewLayout(PreviewLayout.sizeThatFits) } } From a79f769ed06897c89a84a01c57b3f704a1d7d53e Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 29 Dec 2022 21:53:22 -0600 Subject: [PATCH 07/18] feat: delete tweet --- brain-marks/Tweets/TweetListViewModel.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/brain-marks/Tweets/TweetListViewModel.swift b/brain-marks/Tweets/TweetListViewModel.swift index 9975794..c920aa8 100644 --- a/brain-marks/Tweets/TweetListViewModel.swift +++ b/brain-marks/Tweets/TweetListViewModel.swift @@ -8,6 +8,7 @@ import SwiftUI final class TweetListViewModel: ObservableObject { + private let storageProvider = StorageProvider.shared @Published var tweets = [TweetEntity]() @@ -21,7 +22,12 @@ final class TweetListViewModel: ObservableObject { for _ in offsets { offsets.sorted(by: >).forEach { index in let tweet = tweets[index] - DataStoreManger.shared.deleteTweet(tweet) + storageProvider.context.delete(tweet) + do { + try storageProvider.context.save() + } catch { + print("❌ TweetListViewModel.deleteTweet(at:) Error \(error)") + } } } tweets.remove(atOffsets: offsets) From 01091c4839cb10289f31b6815a736879f23760c3 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 29 Dec 2022 21:57:03 -0600 Subject: [PATCH 08/18] feat: remove category --- brain-marks/Categories/CategoryListViewModel.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/brain-marks/Categories/CategoryListViewModel.swift b/brain-marks/Categories/CategoryListViewModel.swift index 1eb564a..30b97e7 100644 --- a/brain-marks/Categories/CategoryListViewModel.swift +++ b/brain-marks/Categories/CategoryListViewModel.swift @@ -37,6 +37,17 @@ final class CategoryListViewModel: ObservableObject { } func deleteCategory(at offsets: IndexSet) { + for _ in offsets { + offsets.sorted(by: >).forEach { index in + let category = categories[index] + storageProvider.context.delete(category) + do { + try storageProvider.context.save() + } catch { + print("❌ CategoryListViewModel.deleteCategory(at:) Error \(error)") + } + } + } categories.remove(atOffsets: offsets) } From 2e6e1f62cc5537528dbf90686e492b89a78a33a1 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 29 Dec 2022 22:20:55 -0600 Subject: [PATCH 09/18] feat: determines if migration needs to occur --- brain-marks/ContentView.swift | 12 +++++------ brain-marks/Services/MigrationService.swift | 22 +++++++++++++-------- brain-marks/brain_marksApp.swift | 8 ++++---- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/brain-marks/ContentView.swift b/brain-marks/ContentView.swift index ef5b87e..b093d63 100644 --- a/brain-marks/ContentView.swift +++ b/brain-marks/ContentView.swift @@ -8,24 +8,22 @@ import SwiftUI struct ContentView: View { - let migrationService: MigrationService + let migrationService: MigrationService = MigrationService() @State private var showAddSheet = false - init(storageProvider: StorageProvider) { - self.migrationService = MigrationService(managedObjectContext: storageProvider.context) - } - var body: some View { CategoryList() .onAppear { - migrationService.performMigration() + if migrationService.checkIfMigrationShouldRun() { + migrationService.performMigration() + } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView(storageProvider: StorageProvider()) + ContentView() } } diff --git a/brain-marks/Services/MigrationService.swift b/brain-marks/Services/MigrationService.swift index 5debe5a..3b038dc 100644 --- a/brain-marks/Services/MigrationService.swift +++ b/brain-marks/Services/MigrationService.swift @@ -10,18 +10,15 @@ import Foundation /// Controls the migration from Amplify to CoreData class MigrationService { - /// Need to get category from Amplify - /// Store Category inside CD - /// Loop through category and get tweets - /// Store tweet in CD and associate to proper category + private let storageProvider = StorageProvider.shared private var awsCategories: [AWSCategory] = [] private let amplifyDataStore = DataStoreManger.shared private var managedObjectContext: NSManagedObjectContext - init(managedObjectContext: NSManagedObjectContext) { - self.managedObjectContext = managedObjectContext + init() { + self.managedObjectContext = storageProvider.context } func performMigration() { @@ -38,12 +35,21 @@ class MigrationService { } } do { - try managedObjectContext.save() + try managedObjectContext.save() } catch { print("❌ MigrationService.performMigration() Error: \(error)") } } + UserDefaults.standard.set(true, forKey: "migrationToCoreDataRan") + } + + public func checkIfMigrationShouldRun() -> Bool { + let migrationToCoreDataRan = UserDefaults.standard.bool(forKey: "migrationToCoreDataRan") + if storageProvider.getAllCategories().isEmpty && !migrationToCoreDataRan { + return true + } + return false } private func getCategories() { @@ -52,7 +58,7 @@ class MigrationService { case .success(let categories): self.awsCategories = categories case .failure(let error): - print("MigrationService.getCategories(): Error: \(error)") + print("❌ MigrationService.getCategories(): Error: \(error)") } }) } diff --git a/brain-marks/brain_marksApp.swift b/brain-marks/brain_marksApp.swift index 39ec075..613a261 100644 --- a/brain-marks/brain_marksApp.swift +++ b/brain-marks/brain_marksApp.swift @@ -12,7 +12,9 @@ import TelemetryClient @main struct brain_marksApp: App { - + @AppStorage("migrationToCoreDataRan") + private var migrationToCoreDataRan: Bool = false + init() { configureAmplify() @@ -21,11 +23,9 @@ struct brain_marksApp: App { TelemetryManager.send(TelemetrySignals.appLaunchedRegularly) } - let storageProvider = StorageProvider.shared - var body: some Scene { WindowGroup { - ContentView(storageProvider: storageProvider) + ContentView() } } From 11f6e2f330b4423b5b0ffe5bebb2ecca2eeb0fbf Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 29 Dec 2022 22:47:39 -0600 Subject: [PATCH 10/18] feat: adding new category works --- .../Categories/Views/CategorySheetView.swift | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/brain-marks/Categories/Views/CategorySheetView.swift b/brain-marks/Categories/Views/CategorySheetView.swift index 1538a3b..6c0dd80 100644 --- a/brain-marks/Categories/Views/CategorySheetView.swift +++ b/brain-marks/Categories/Views/CategorySheetView.swift @@ -17,6 +17,8 @@ struct CategorySheetView: View { @State private var category = "" @State private var title = "" + + private let storageProvider = StorageProvider.shared var body: some View { NavigationView { @@ -54,10 +56,7 @@ struct CategorySheetView: View { switch categorySheetState { case .new: - DataStoreManger.shared.createCategory( - category: AWSCategory(name: category, - imageName: "folder")) - TelemetryManager.send(TelemetrySignals.addCategory) + addNewCategory() case .edit: guard editCategory != nil else { return @@ -87,6 +86,22 @@ struct CategorySheetView: View { } } } + + func addNewCategory() { + let newCategory = CategoryEntity(context: storageProvider.context) + newCategory.id = UUID() + newCategory.dateCreated = Date() + newCategory.dateModified = Date() + newCategory.name = category + newCategory.imageName = "folder" + + do { + try storageProvider.context.save() + TelemetryManager.send(TelemetrySignals.addCategory) + } catch { + print("❌ CategorySheetView.addNewCategory: \(error)") + } + } } struct NewCategorySheetView_Previews: PreviewProvider { From 9ec01254870053937a96db86008af02d959ce90b Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 29 Dec 2022 23:03:23 -0600 Subject: [PATCH 11/18] can save tweet --- brain-marks/Add/AddURLView.swift | 49 +++++++++++-------- .../Categories/Views/CategoryList.swift | 2 +- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/brain-marks/Add/AddURLView.swift b/brain-marks/Add/AddURLView.swift index 490229f..d198fc6 100644 --- a/brain-marks/Add/AddURLView.swift +++ b/brain-marks/Add/AddURLView.swift @@ -11,14 +11,21 @@ import UIKit struct AddURLView: View { @State private var showingAlert = false - @State private var selectedCategory = AWSCategory(name: "") + @State private var selectedCategory: CategoryEntity @State var newEntry = "" @Environment(\.presentationMode) var presentationMode - let categories: [AWSCategory] + let categories: [CategoryEntity] @StateObject var viewModel = AddURLViewModel() let pasteBoard = UIPasteboard.general + + private let storageProvider = StorageProvider.shared + + init(categories: [CategoryEntity]) { + self.categories = categories + self._selectedCategory = State(initialValue: categories.first!) + } var body: some View { NavigationView { @@ -26,34 +33,34 @@ struct AddURLView: View { TextField("Enter copied url", text: $newEntry) .autocapitalization(.none) Picker(selection: $selectedCategory , label: Text("Category"), content: { - ForEach(categories,id:\.self) { category in - Text(category.name).tag(category.id) + ForEach(categories, id:\.self) { category in + Text(category.name ?? "No name").tag(category) } }) } .navigationBarItems( trailing: Button("Save") { - if selectedCategory.name == "" { - viewModel.alertItem = AlertContext.noCategory - showingAlert = true - } else { - viewModel.fetchTweet(url: newEntry) { result in - switch result { - case .success(let tweet): - - DataStoreManger.shared.fetchCategories { (result) in - if case .success(_) = result { - DataStoreManger.shared.createTweet( - tweet: tweet, - category: selectedCategory) - } + viewModel.fetchTweet(url: newEntry) { result in + switch result { + case .success(let tweet): + let tweetEntity = TweetEntity(context: storageProvider.context) + tweetEntity.authorName = tweet.authorName + tweetEntity.authorUsername = tweet.authorUsername + tweetEntity.dateCreated = Date() + tweetEntity.id = UUID() + tweetEntity.profileImageURL = tweet.profileImageURL + tweetEntity.text = tweet.text + tweetEntity.tweetID = tweet.id + selectedCategory.addToTweets(tweetEntity) + do { + try storageProvider.context.save() TelemetryManager.send(TelemetrySignals.addTweet) presentationMode.wrappedValue.dismiss() + } catch { + print("❌ AddURLView.save() Error: \(error)") } - - case .failure(_): + case .failure: viewModel.alertItem = AlertContext.badURL - } } } }) diff --git a/brain-marks/Categories/Views/CategoryList.swift b/brain-marks/Categories/Views/CategoryList.swift index 6df0d75..c3c209f 100644 --- a/brain-marks/Categories/Views/CategoryList.swift +++ b/brain-marks/Categories/Views/CategoryList.swift @@ -50,7 +50,7 @@ struct CategoryList: View { Image(systemName:"plus.circle") } .sheet(isPresented: $showAddURLView) { -// AddURLView(categories: viewModel.categories) + AddURLView(categories: viewModel.categories) } } } From 87aadc4334f2706e0a019aa0535198b070e68ff8 Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 30 Dec 2022 21:50:39 -0600 Subject: [PATCH 12/18] =?UTF-8?q?feat:=20edit=20works=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Categories/CategoryListViewModel.swift | 11 +++--- .../Categories/Views/CategoryList.swift | 13 ++++--- .../Categories/Views/CategoryRow.swift | 13 ++++--- .../Categories/Views/CategorySheetView.swift | 38 +++++++++++-------- .../CoreData/StoreageProvider+Category.swift | 1 + 5 files changed, 45 insertions(+), 31 deletions(-) diff --git a/brain-marks/Categories/CategoryListViewModel.swift b/brain-marks/Categories/CategoryListViewModel.swift index 30b97e7..9248dcf 100644 --- a/brain-marks/Categories/CategoryListViewModel.swift +++ b/brain-marks/Categories/CategoryListViewModel.swift @@ -11,7 +11,7 @@ final class CategoryListViewModel: ObservableObject { let storageProvider: StorageProvider - @Published var categories = [CategoryEntity]() + @Published var categories: [CategoryEntity] init() { /// Using a compiler statement here to determine where the code is being ran. @@ -30,6 +30,7 @@ final class CategoryListViewModel: ObservableObject { #else storageProvider = .shared #endif + categories = [] } func getCategories() { @@ -48,10 +49,8 @@ final class CategoryListViewModel: ObservableObject { } } } - categories.remove(atOffsets: offsets) - } - - func editCategoryName(category: AWSCategory, newName: String) { - DataStoreManger.shared.editCategory(category: category, newName: newName) + withAnimation { + categories.remove(atOffsets: offsets) + } } } diff --git a/brain-marks/Categories/Views/CategoryList.swift b/brain-marks/Categories/Views/CategoryList.swift index c3c209f..24d86c0 100644 --- a/brain-marks/Categories/Views/CategoryList.swift +++ b/brain-marks/Categories/Views/CategoryList.swift @@ -14,13 +14,13 @@ enum CategoryState { struct CategoryList: View { @State private var categorySheetState: CategoryState = .new - @State private var editCategory: AWSCategory? + @State private var editCategory: CategoryEntity? @State private var indexSetToDelete: IndexSet? @State private var showAddURLView = false @State private var showingCategorySheet = false @State private var showingDeleteActionSheet = false - @StateObject var viewModel = CategoryListViewModel() + @StateObject private var viewModel = CategoryListViewModel() var body: some View { NavigationView { @@ -75,13 +75,16 @@ struct CategoryList: View { var categories: some View { List { - ForEach(viewModel.categories, id: \.id) { category in + ForEach(viewModel.categories, id: \.self) { category in NavigationLink(destination: TweetList(category: category)) { - CategoryRow(category: category) + CategoryRow( + categoryName: category.name ?? "", + categoryImage: category.imageName ?? "folder" + ) } .contextMenu { Button { -// editCategory = category + editCategory = category categorySheetState = .edit showingCategorySheet.toggle() } label: { diff --git a/brain-marks/Categories/Views/CategoryRow.swift b/brain-marks/Categories/Views/CategoryRow.swift index 0a4356d..4ec6b08 100644 --- a/brain-marks/Categories/Views/CategoryRow.swift +++ b/brain-marks/Categories/Views/CategoryRow.swift @@ -8,15 +8,16 @@ import SwiftUI struct CategoryRow: View { - let category: CategoryEntity + let categoryName: String + let categoryImage: String var body: some View { HStack { - Image(systemName: category.imageName ?? "folder") + Image(systemName: categoryImage) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) - Text(category.name ?? "No name") + Text(categoryName) Spacer() } } @@ -26,10 +27,12 @@ struct CategoryRow_Previews: PreviewProvider { static var previews: some View { Group { CategoryRow( - category: StorageProvider.preview.getAllCategories()[0] + categoryName: "iOS Tips and Tricks", + categoryImage: "folder" ) CategoryRow( - category: StorageProvider.preview.getAllCategories()[1] + categoryName: "macOS Tips and Tricks", + categoryImage: "swift" ) } .previewLayout(.fixed(width: 300, height: 70)) diff --git a/brain-marks/Categories/Views/CategorySheetView.swift b/brain-marks/Categories/Views/CategorySheetView.swift index 6c0dd80..fc766ff 100644 --- a/brain-marks/Categories/Views/CategorySheetView.swift +++ b/brain-marks/Categories/Views/CategorySheetView.swift @@ -10,7 +10,7 @@ import TelemetryClient struct CategorySheetView: View { - @Binding var editCategory: AWSCategory? + @Binding var editCategory: CategoryEntity? @Binding var categorySheetState: CategoryState @Environment(\.presentationMode) var presentationMode @@ -23,7 +23,6 @@ struct CategorySheetView: View { var body: some View { NavigationView { VStack { - switch categorySheetState { case .new: TextField("Enter name of new category", text: $category) @@ -50,21 +49,13 @@ struct CategorySheetView: View { } Button { - presentationMode.wrappedValue.dismiss() - if !category.isEmpty { switch categorySheetState { case .new: addNewCategory() case .edit: - guard editCategory != nil else { - return - } - - DataStoreManger.shared.editCategory( - category: editCategory!, - newName: category) + performEdit() } } } label: { @@ -98,18 +89,35 @@ struct CategorySheetView: View { do { try storageProvider.context.save() TelemetryManager.send(TelemetrySignals.addCategory) + presentationMode.wrappedValue.dismiss() } catch { print("❌ CategorySheetView.addNewCategory: \(error)") } } + + func performEdit() { + guard let editCategory else { + return + } + editCategory.dateModified = Date() + editCategory.name = category + + do { + try storageProvider.context.save() + presentationMode.wrappedValue.dismiss() + } catch { + print("❌ CategorySheetView.performEdit() error: \(error)") + } + } } struct NewCategorySheetView_Previews: PreviewProvider { static var previews: some View { CategorySheetView( - editCategory: .constant(AWSCategory(id: "1", - name: "CategoryName", - imageName: "swift")), - categorySheetState: .constant(.new)) + editCategory: .constant( + StorageProvider.preview.getAllCategories()[1] + ), + categorySheetState: .constant(.edit) + ) } } diff --git a/brain-marks/CoreData/StoreageProvider+Category.swift b/brain-marks/CoreData/StoreageProvider+Category.swift index 017539a..47d14e9 100644 --- a/brain-marks/CoreData/StoreageProvider+Category.swift +++ b/brain-marks/CoreData/StoreageProvider+Category.swift @@ -21,6 +21,7 @@ extension StorageProvider { func getAllCategories() -> [CategoryEntity] { let fetchRequest: NSFetchRequest = CategoryEntity.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "dateModified", ascending: false)] do { return try context.fetch(fetchRequest) From 26669e5c5888edeb175cda957ed9e63c02048c03 Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 30 Dec 2022 22:00:56 -0600 Subject: [PATCH 13/18] fix: category name erasing --- brain-marks/Add/AddURLView.swift | 7 ++++--- brain-marks/Categories/Views/CategoryList.swift | 3 +++ brain-marks/Tweets/TweetListViewModel.swift | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/brain-marks/Add/AddURLView.swift b/brain-marks/Add/AddURLView.swift index d198fc6..014843b 100644 --- a/brain-marks/Add/AddURLView.swift +++ b/brain-marks/Add/AddURLView.swift @@ -51,7 +51,11 @@ struct AddURLView: View { tweetEntity.profileImageURL = tweet.profileImageURL tweetEntity.text = tweet.text tweetEntity.tweetID = tweet.id + + // Category edits selectedCategory.addToTweets(tweetEntity) + selectedCategory.dateModified = Date() + do { try storageProvider.context.save() TelemetryManager.send(TelemetrySignals.addTweet) @@ -70,9 +74,6 @@ struct AddURLView: View { newEntry = pasteBoard.string ?? "" } } - .onDisappear { - selectedCategory.name = "" - } .alert(item: $viewModel.alertItem) { alertItem in Alert(title: Text(alertItem.title), message: Text(alertItem.message), diff --git a/brain-marks/Categories/Views/CategoryList.swift b/brain-marks/Categories/Views/CategoryList.swift index 24d86c0..8b01c19 100644 --- a/brain-marks/Categories/Views/CategoryList.swift +++ b/brain-marks/Categories/Views/CategoryList.swift @@ -51,6 +51,9 @@ struct CategoryList: View { } .sheet(isPresented: $showAddURLView) { AddURLView(categories: viewModel.categories) + .onDisappear { + viewModel.getCategories() + } } } } diff --git a/brain-marks/Tweets/TweetListViewModel.swift b/brain-marks/Tweets/TweetListViewModel.swift index c920aa8..8e6f61c 100644 --- a/brain-marks/Tweets/TweetListViewModel.swift +++ b/brain-marks/Tweets/TweetListViewModel.swift @@ -16,6 +16,7 @@ final class TweetListViewModel: ObservableObject { for tweet in tweets { self.tweets.append(tweet as! TweetEntity) } + self.tweets.sort { $0.dateCreated ?? Date() > $1.dateCreated ?? Date() } } func deleteTweet(at offsets: IndexSet) { From 196ca6cfdfa5cc1bbb195d916a553086f9817f6c Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 30 Dec 2022 22:47:29 -0600 Subject: [PATCH 14/18] Adjusts default value --- brain-marks/Tweets/Views/TweetCard.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brain-marks/Tweets/Views/TweetCard.swift b/brain-marks/Tweets/Views/TweetCard.swift index a2230af..ef018e2 100644 --- a/brain-marks/Tweets/Views/TweetCard.swift +++ b/brain-marks/Tweets/Views/TweetCard.swift @@ -14,7 +14,7 @@ struct TweetCard: View { var body: some View { VStack(alignment: .leading) { TweetHeaderView(tweet: tweet) - TweetBodyView(tweetBody: tweet.text ?? "help") + TweetBodyView(tweetBody: tweet.text ?? "") // TweetFooterView() } } From 645803dd1fd3a5c14e5431dfb2782955c9a40cfe Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 20 Jan 2023 09:15:10 -0600 Subject: [PATCH 15/18] fix spelling --- brain-marks/Categories/CategoryListViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brain-marks/Categories/CategoryListViewModel.swift b/brain-marks/Categories/CategoryListViewModel.swift index 9248dcf..374a6cb 100644 --- a/brain-marks/Categories/CategoryListViewModel.swift +++ b/brain-marks/Categories/CategoryListViewModel.swift @@ -21,7 +21,7 @@ final class CategoryListViewModel: ObservableObject { /// If it's being ran in the canvas (preview) then we want to use the preview container /// If it's being ran not in the canvas, then we want to use the CoreData file for existing data. #if DEBUG - // Checks if the code is running in the cancas (preview) + // Checks if the code is running in the canvas (preview) if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { storageProvider = .preview } else { From 3d623d9ac67e5a7107b491adf20dc8447213d5b6 Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 20 Jan 2023 09:20:11 -0600 Subject: [PATCH 16/18] pr requests --- brain-marks.xcodeproj/project.pbxproj | 8 ++++---- .../Categories/Views/CategoryList.swift | 5 +---- .../Categories/Views/CategoryRow.swift | 19 +++++++------------ brain-marks/ContentView.swift | 4 +++- ...y.swift => StorageProvider+Category.swift} | 4 ++-- brain-marks/CoreData/StorageProvider.swift | 4 ++-- brain-marks/Services/MigrationService.swift | 12 ++++++------ brain-marks/TelemetrySignals.swift | 3 +++ 8 files changed, 28 insertions(+), 31 deletions(-) rename brain-marks/CoreData/{StoreageProvider+Category.swift => StorageProvider+Category.swift} (87%) diff --git a/brain-marks.xcodeproj/project.pbxproj b/brain-marks.xcodeproj/project.pbxproj index 1729bbb..08f5382 100644 --- a/brain-marks.xcodeproj/project.pbxproj +++ b/brain-marks.xcodeproj/project.pbxproj @@ -11,7 +11,7 @@ 684E2F2D3FCC4D36AC68E57D /* AWSCategory+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551BA45F30D640118D2F0CE2 /* AWSCategory+Schema.swift */; }; 6961687B2949756D00D68070 /* BrainMarks.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 696168792949756D00D68070 /* BrainMarks.xcdatamodeld */; }; 6961687F294976A600D68070 /* StorageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6961687E294976A600D68070 /* StorageProvider.swift */; }; - 696168812949856100D68070 /* StoreageProvider+Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696168802949856100D68070 /* StoreageProvider+Category.swift */; }; + 696168812949856100D68070 /* StorageProvider+Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696168802949856100D68070 /* StorageProvider+Category.swift */; }; 69616883294988E700D68070 /* StorageProvider+Tweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69616882294988E700D68070 /* StorageProvider+Tweet.swift */; }; 6961688629498D0600D68070 /* MigrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6961688529498D0600D68070 /* MigrationService.swift */; }; 91739DEB2622D2A7000F982A /* AddURLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91739DEA2622D2A7000F982A /* AddURLView.swift */; }; @@ -79,7 +79,7 @@ 63D0518628EF45A382F08352 /* AWSCategory.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = AWSCategory.swift; path = amplify/generated/models/AWSCategory.swift; sourceTree = ""; }; 6961687A2949756D00D68070 /* BrainMarks.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = BrainMarks.xcdatamodel; sourceTree = ""; }; 6961687E294976A600D68070 /* StorageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageProvider.swift; sourceTree = ""; }; - 696168802949856100D68070 /* StoreageProvider+Category.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoreageProvider+Category.swift"; sourceTree = ""; }; + 696168802949856100D68070 /* StorageProvider+Category.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageProvider+Category.swift"; sourceTree = ""; }; 69616882294988E700D68070 /* StorageProvider+Tweet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageProvider+Tweet.swift"; sourceTree = ""; }; 6961688529498D0600D68070 /* MigrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationService.swift; sourceTree = ""; }; 91739DEA2622D2A7000F982A /* AddURLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddURLView.swift; sourceTree = ""; }; @@ -171,7 +171,7 @@ 696168792949756D00D68070 /* BrainMarks.xcdatamodeld */, 6961687E294976A600D68070 /* StorageProvider.swift */, 69616882294988E700D68070 /* StorageProvider+Tweet.swift */, - 696168802949856100D68070 /* StoreageProvider+Category.swift */, + 696168802949856100D68070 /* StorageProvider+Category.swift */, ); path = CoreData; sourceTree = ""; @@ -542,7 +542,7 @@ CF16AFB9BE3F401E98C85114 /* AWSTweet.swift in Sources */, FF36B661262357F9007A6D7F /* AWSCategory+Extension.swift in Sources */, FFD5152E28F9FAA500665625 /* TelemetrySignals.swift in Sources */, - 696168812949856100D68070 /* StoreageProvider+Category.swift in Sources */, + 696168812949856100D68070 /* StorageProvider+Category.swift in Sources */, FF36B66C26235FE6007A6D7F /* TweetListViewModel.swift in Sources */, C4392E80BFE640DAA2625F9D /* AWSTweet+Schema.swift in Sources */, A205CD422622A3EB00517DB5 /* TweetList.swift in Sources */, diff --git a/brain-marks/Categories/Views/CategoryList.swift b/brain-marks/Categories/Views/CategoryList.swift index 8b01c19..764d95f 100644 --- a/brain-marks/Categories/Views/CategoryList.swift +++ b/brain-marks/Categories/Views/CategoryList.swift @@ -80,10 +80,7 @@ struct CategoryList: View { List { ForEach(viewModel.categories, id: \.self) { category in NavigationLink(destination: TweetList(category: category)) { - CategoryRow( - categoryName: category.name ?? "", - categoryImage: category.imageName ?? "folder" - ) + CategoryRow(category: category) } .contextMenu { Button { diff --git a/brain-marks/Categories/Views/CategoryRow.swift b/brain-marks/Categories/Views/CategoryRow.swift index 4ec6b08..c826b58 100644 --- a/brain-marks/Categories/Views/CategoryRow.swift +++ b/brain-marks/Categories/Views/CategoryRow.swift @@ -8,16 +8,15 @@ import SwiftUI struct CategoryRow: View { - let categoryName: String - let categoryImage: String + let category: CategoryEntity var body: some View { HStack { - Image(systemName: categoryImage) + Image(systemName: category.imageName ?? "folder") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) - Text(categoryName) + Text(category.name ?? "Category name not found") Spacer() } } @@ -25,15 +24,11 @@ struct CategoryRow: View { struct CategoryRow_Previews: PreviewProvider { static var previews: some View { + let context = StorageProvider.preview + let items = context.getAllCategories() Group { - CategoryRow( - categoryName: "iOS Tips and Tricks", - categoryImage: "folder" - ) - CategoryRow( - categoryName: "macOS Tips and Tricks", - categoryImage: "swift" - ) + CategoryRow(category: items.first!) + CategoryRow(category: items.first!) } .previewLayout(.fixed(width: 300, height: 70)) } diff --git a/brain-marks/ContentView.swift b/brain-marks/ContentView.swift index b093d63..f7aa2c4 100644 --- a/brain-marks/ContentView.swift +++ b/brain-marks/ContentView.swift @@ -6,9 +6,10 @@ // import SwiftUI +import TelemetryClient struct ContentView: View { - let migrationService: MigrationService = MigrationService() + let migrationService = MigrationService() @State private var showAddSheet = false @@ -17,6 +18,7 @@ struct ContentView: View { .onAppear { if migrationService.checkIfMigrationShouldRun() { migrationService.performMigration() + TelemetryManager.send(TelemetrySignals.migrationFromAmplifyToCDPerformed) } } } diff --git a/brain-marks/CoreData/StoreageProvider+Category.swift b/brain-marks/CoreData/StorageProvider+Category.swift similarity index 87% rename from brain-marks/CoreData/StoreageProvider+Category.swift rename to brain-marks/CoreData/StorageProvider+Category.swift index 47d14e9..368655e 100644 --- a/brain-marks/CoreData/StoreageProvider+Category.swift +++ b/brain-marks/CoreData/StorageProvider+Category.swift @@ -1,5 +1,5 @@ // -// StoreageProvider+Category.swift +// StorageProvider+Category.swift // brain-marks // // Created by Jay on 12/13/22. @@ -26,7 +26,7 @@ extension StorageProvider { do { return try context.fetch(fetchRequest) } catch { - print("StorageProvider.getAllCategories(:): Error \(error)") + print("❌ StorageProvider.getAllCategories(:): Error \(error)") return [] } } diff --git a/brain-marks/CoreData/StorageProvider.swift b/brain-marks/CoreData/StorageProvider.swift index 0d3931e..b00c20f 100644 --- a/brain-marks/CoreData/StorageProvider.swift +++ b/brain-marks/CoreData/StorageProvider.swift @@ -9,7 +9,7 @@ import Foundation import CoreData import TelemetryClient -class StorageProvider { +final class StorageProvider { static let shared = StorageProvider() @@ -35,7 +35,7 @@ class StorageProvider { return controller }() - init(inMemory: Bool = false) { + private init(inMemory: Bool = false) { container = NSPersistentContainer(name: "BrainMarks") if inMemory { diff --git a/brain-marks/Services/MigrationService.swift b/brain-marks/Services/MigrationService.swift index 3b038dc..6bb23bb 100644 --- a/brain-marks/Services/MigrationService.swift +++ b/brain-marks/Services/MigrationService.swift @@ -9,7 +9,7 @@ import CoreData import Foundation /// Controls the migration from Amplify to CoreData -class MigrationService { +final class MigrationService { private let storageProvider = StorageProvider.shared private var awsCategories: [AWSCategory] = [] @@ -27,10 +27,10 @@ class MigrationService { /// 2. Go through each category and get tweets for awsCategory in awsCategories { /// 3. Add category to core data - let categoryToAdd = makeCategoryCoreDataCompatible(category: awsCategory) + let categoryToAdd = convertAmplifyCategoryToCoreDataCategory(category: awsCategory) amplifyDataStore.fetchSavedTweets(for: awsCategory) { awsTweets in for tweet in awsTweets ?? [] { - let tweetToAdd = self.makeTweetCoreDataCompatible(tweet: tweet) + let tweetToAdd = self.convertAmplifyTweetToCoreDataTweet(tweet: tweet) categoryToAdd.addToTweets(tweetToAdd) } } @@ -43,7 +43,7 @@ class MigrationService { UserDefaults.standard.set(true, forKey: "migrationToCoreDataRan") } - public func checkIfMigrationShouldRun() -> Bool { + func checkIfMigrationShouldRun() -> Bool { let migrationToCoreDataRan = UserDefaults.standard.bool(forKey: "migrationToCoreDataRan") if storageProvider.getAllCategories().isEmpty && !migrationToCoreDataRan { @@ -63,7 +63,7 @@ class MigrationService { }) } - private func makeCategoryCoreDataCompatible(category: AWSCategory) -> CategoryEntity { + private func convertAmplifyCategoryToCoreDataCategory(category: AWSCategory) -> CategoryEntity { let tweetCat = CategoryEntity(context: managedObjectContext) tweetCat.amplifyID = category.id tweetCat.id = UUID() @@ -75,7 +75,7 @@ class MigrationService { return tweetCat } - private func makeTweetCoreDataCompatible(tweet: AWSTweet) -> TweetEntity { + private func convertAmplifyTweetToCoreDataTweet(tweet: AWSTweet) -> TweetEntity { let tweetEntity = TweetEntity(context: managedObjectContext) tweetEntity.id = UUID(uuidString: tweet.id) tweetEntity.tweetID = tweet.tweetID diff --git a/brain-marks/TelemetrySignals.swift b/brain-marks/TelemetrySignals.swift index 9fa4954..3b118a0 100644 --- a/brain-marks/TelemetrySignals.swift +++ b/brain-marks/TelemetrySignals.swift @@ -21,4 +21,7 @@ enum TelemetrySignals { /// Core Data failed to load static let errorCoreDataLoad = "errorCoreDataLoad" + + /// Migration performed + static let migrationFromAmplifyToCDPerformed = "migrationFromAmplifyToCDPerformed" } From 67cba4d1317e9123f2ee0c7cf0f4cefa73929dbb Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 20 Jan 2023 09:50:47 -0600 Subject: [PATCH 17/18] refactor: allows for fetch to happen before adding to core data --- brain-marks/Services/MigrationService.swift | 26 +++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/brain-marks/Services/MigrationService.swift b/brain-marks/Services/MigrationService.swift index 6bb23bb..2ba0899 100644 --- a/brain-marks/Services/MigrationService.swift +++ b/brain-marks/Services/MigrationService.swift @@ -22,11 +22,19 @@ final class MigrationService { } func performMigration() { - /// 1. Fetch categories - getCategories() - /// 2. Go through each category and get tweets + amplifyDataStore.fetchCategories(completion: { result in + switch result { + case .success(let categories): + self.awsCategories = categories + self.addToCoreData() + case .failure(let error): + print("❌ MigrationService.getCategories(): Error: \(error)") + } + }) + } + + private func addToCoreData() { for awsCategory in awsCategories { - /// 3. Add category to core data let categoryToAdd = convertAmplifyCategoryToCoreDataCategory(category: awsCategory) amplifyDataStore.fetchSavedTweets(for: awsCategory) { awsTweets in for tweet in awsTweets ?? [] { @@ -43,6 +51,7 @@ final class MigrationService { UserDefaults.standard.set(true, forKey: "migrationToCoreDataRan") } + func checkIfMigrationShouldRun() -> Bool { let migrationToCoreDataRan = UserDefaults.standard.bool(forKey: "migrationToCoreDataRan") @@ -53,14 +62,7 @@ final class MigrationService { } private func getCategories() { - amplifyDataStore.fetchCategories(completion: { result in - switch result { - case .success(let categories): - self.awsCategories = categories - case .failure(let error): - print("❌ MigrationService.getCategories(): Error: \(error)") - } - }) + } private func convertAmplifyCategoryToCoreDataCategory(category: AWSCategory) -> CategoryEntity { From 54f94bcacd9e06a9f693875e5d1e745dab2ba115 Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 20 Jan 2023 21:43:13 -0600 Subject: [PATCH 18/18] =?UTF-8?q?forgot=20to=20save=20these=20=F0=9F=A4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- brain-marks/CoreData/StorageProvider.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/brain-marks/CoreData/StorageProvider.swift b/brain-marks/CoreData/StorageProvider.swift index b00c20f..669bd17 100644 --- a/brain-marks/CoreData/StorageProvider.swift +++ b/brain-marks/CoreData/StorageProvider.swift @@ -32,6 +32,14 @@ final class StorageProvider { category.addToTweets(tweet) } } + do { + try controller.context.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } return controller }()