From 906cf57a251fb6ec475c1db56b4d21fc516a3a50 Mon Sep 17 00:00:00 2001 From: Stephanie Ramirez | Fueled Date: Tue, 16 Jul 2024 19:01:35 -0400 Subject: [PATCH 1/5] feat(FirebaseError): Error handling attempt --- FairShare.xcodeproj/project.pbxproj | 4 + FairShare/Errors/FirebaseErrors.swift | 181 +++++++++++++++--- .../Helpers/Extensions/NSError+Helper.swift | 100 ++++++++++ FairShare/Models/AuthViewModel.swift | 34 ++-- FairShare/Networking/AuthService.swift | 11 +- 5 files changed, 283 insertions(+), 47 deletions(-) create mode 100644 FairShare/Helpers/Extensions/NSError+Helper.swift diff --git a/FairShare.xcodeproj/project.pbxproj b/FairShare.xcodeproj/project.pbxproj index bd4f7ce..0892f23 100644 --- a/FairShare.xcodeproj/project.pbxproj +++ b/FairShare.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ 909E69A02C02C865009D00D6 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909E699F2C02C865009D00D6 /* Constants.swift */; }; 90B98CC32C437B94006B6B83 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B98CC22C437B94006B6B83 /* MessageView.swift */; }; 90B98CC52C439451006B6B83 /* String+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B98CC42C439451006B6B83 /* String+Helper.swift */; }; + 90B98CC12C434443006B6B83 /* NSError+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B98CC02C434443006B6B83 /* NSError+Helper.swift */; }; 90BD783A2C0AEEB300C6A889 /* ReceiptItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90BD78392C0AEEB300C6A889 /* ReceiptItemsView.swift */; }; 90BD783C2C0C051E00C6A889 /* EditableReceiptItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90BD783B2C0C051E00C6A889 /* EditableReceiptItemView.swift */; }; 90CA9FB92C396E11009F1251 /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CA9FB82C396E11009F1251 /* SideMenuView.swift */; }; @@ -121,6 +122,7 @@ 909E699F2C02C865009D00D6 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 90B98CC22C437B94006B6B83 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 90B98CC42C439451006B6B83 /* String+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Helper.swift"; sourceTree = ""; }; + 90B98CC02C434443006B6B83 /* NSError+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Helper.swift"; sourceTree = ""; }; 90BD78392C0AEEB300C6A889 /* ReceiptItemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptItemsView.swift; sourceTree = ""; }; 90BD783B2C0C051E00C6A889 /* EditableReceiptItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableReceiptItemView.swift; sourceTree = ""; }; 90CA9FB82C396E11009F1251 /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = ""; }; @@ -368,6 +370,7 @@ 90676E972C3A5CDA00828276 /* Double+Helper.swift */, 909E699B2C02C820009D00D6 /* Image+Helper.swift */, 90B98CC42C439451006B6B83 /* String+Helper.swift */, + 90B98CC02C434443006B6B83 /* NSError+Helper.swift */, ); path = Extensions; sourceTree = ""; @@ -551,6 +554,7 @@ 90FB962D2BF0753E0061E83D /* ProfileView.swift in Sources */, 90BD783C2C0C051E00C6A889 /* EditableReceiptItemView.swift in Sources */, 9072AFC72BE9C46E00DE6FEB /* EntryView.swift in Sources */, + 90B98CC12C434443006B6B83 /* NSError+Helper.swift in Sources */, 90F4761F2C154FE20075B36B /* AuthService.swift in Sources */, 90FB96172BF023340061E83D /* Date+Helper.swift in Sources */, 909E69942C02A5C7009D00D6 /* CameraView.swift in Sources */, diff --git a/FairShare/Errors/FirebaseErrors.swift b/FairShare/Errors/FirebaseErrors.swift index a467e22..2f014a3 100644 --- a/FairShare/Errors/FirebaseErrors.swift +++ b/FairShare/Errors/FirebaseErrors.swift @@ -7,10 +7,13 @@ import Foundation import Firebase +import FirebaseStorage -protocol DBError: Error {} +protocol FirebaseError: Error, LocalizedError { -enum FirebaseAuthError: DBError { +} + +enum FirebaseAuthError: FirebaseError { case emailAlreadyInUse case userNotFound case userDisabled @@ -20,32 +23,15 @@ enum FirebaseAuthError: DBError { case wrongPassword case operationNotAllowed case keychainError - case unknownError(String) + case unknownNSError(NSError) + case unknownError(Error) - init(_ error: NSError) { - let authErrorCode = AuthErrorCode(_nsError: error).code - switch authErrorCode { - case .emailAlreadyInUse: - self = .emailAlreadyInUse - case .userNotFound: - self = .userNotFound - case .userDisabled: - self = .userDisabled - case .invalidEmail, .invalidSender, .invalidRecipientEmail: - self = .invalidEmail - case .networkError: - self = .networkError - case .weakPassword: - self = .weakPassword - case .wrongPassword: - self = .wrongPassword - case .operationNotAllowed: - self = .operationNotAllowed - case .keychainError: - self = .keychainError - default: - self = .unknownError("\(authErrorCode)") + init(_ error: Error) { + if let nsError = error as NSError? { + self = nsError.asFirebaseAuthError() + } else { + self = .unknownError(error) } } @@ -69,8 +55,147 @@ enum FirebaseAuthError: DBError { return "Sign in with this method is currently disabled. Please contact support for further assistance." case .keychainError: return "An error occurred while trying to access secure information. Please try again or contact support if the problem persists." - case .unknownError(let code): - return "Unknown error occurred. Error code: \(code)" + case .unknownError(let error): + return "Unknown Error occurred. Error: \(error)" + case .unknownNSError(let error): + return "Unknown NSError occurred. Error code: \(error.code)" + } + } +} + +enum FirebaseFirestoreError: FirebaseError { + case cancelled + case invalidArgument + case deadlineExceeded + case notFound + case alreadyExists + case permissionDenied + case resourceExhausted + case failedPrecondition + case aborted + case outOfRange + case unimplemented + case internalError + case unavailable + case dataLoss + case unauthenticated + case unknownError(Error) + case unknownNSError(NSError) + + init(_ error: Error) { + if let nsError = error as NSError? { + self = nsError.asFirestoreError() + } else { + self = .unknownError(error) + } + } + + var message: String { + switch self { + case .cancelled: + return "Operation was cancelled." + case .invalidArgument: + return "Invalid argument provided." + case .deadlineExceeded: + return "Deadline for operation exceeded." + case .notFound: + return "Requested document was not found." + case .alreadyExists: + return "Document already exists." + case .permissionDenied: + return "Permission denied." + case .resourceExhausted: + return "Resource exhausted." + case .failedPrecondition: + return "Failed precondition." + case .aborted: + return "Operation aborted." + case .outOfRange: + return "Operation out of range." + case .unimplemented: + return "Operation not implemented." + case .internalError: + return "Internal error occurred." + case .unavailable: + return "Service unavailable." + case .dataLoss: + return "Data loss or corruption." + case .unauthenticated: + return "Unauthenticated request." + case .unknownError(let error): + return "Unknown Error occurred. Error: \(error)" + case .unknownNSError(let error): + return "Unknown NSError occurred. Error code: \(error.code)" + } + } +} + +enum FirebaseStorageError: FirebaseError { + case objectNotFound + case bucketNotFound + case projectNotFound + case quotaExceeded + case unauthenticated + case unauthorized + case retryLimitExceeded + case nonMatchingChecksum + case downloadSizeExceeded + case cancelled + case invalidArgument + case unknownError(Error) + case unknownNSError(NSError) + case unknownStorageError(StorageErrorCode) + + init(_ error: Error) { + if let nsError = error as NSError? { + self = nsError.asFirebaseStorageError() + } else { + self = .unknownError(error) + } + } + + var errorMessage: String { + switch self { + case .retryLimitExceeded: + return "The operation has failed after retrying multiple times. Please try again later." + case .bucketNotFound: + return "The specified bucket could not be found. Please check your configuration." + case .objectNotFound: + return "The specified object could not be found. Please check the object name." + case .quotaExceeded: + return "The quota for Firebase Storage has been exceeded. Please try again later." + case .unauthorized: + return "You are not authorized to perform this operation. Please check your permissions." + case .projectNotFound: + return "The specified project could not be found. Please check your project ID and configuration." + case .unauthenticated: + return "You are not authenticated. Please log in and try again." + case .nonMatchingChecksum: + return "The checksums do not match. The data might be corrupted. Please try again." + case .downloadSizeExceeded: + return "The download size exceeds the allowed limit. Please try downloading a smaller file." + case .cancelled: + return "The operation was cancelled. Please try again if needed." + case .invalidArgument: + return "An invalid argument was provided. Please check the inputs and try again." + case .unknownNSError(let error): + return "An unknown NSError occurred. Error code: \(error.code)" + case .unknownStorageError(let error): + return "An unknown StorageError occurred. Error: \(error)" + case .unknownError(let error): + return "An unknown error occurred. Error: \(error)" + } + } +} + + +enum ImageProcessingError: FirebaseError { + case imageDataConversionFailed + + var errorMessage: String { + switch self { + case .imageDataConversionFailed: + return "Failed to convert the image data." } } } diff --git a/FairShare/Helpers/Extensions/NSError+Helper.swift b/FairShare/Helpers/Extensions/NSError+Helper.swift new file mode 100644 index 0000000..c0e645e --- /dev/null +++ b/FairShare/Helpers/Extensions/NSError+Helper.swift @@ -0,0 +1,100 @@ +// +// NSError+Helper.swift +// FairShare +// +// Created by Stephanie Ramirez on 7/13/24. +// + +import Foundation +import FirebaseAuth +import FirebaseFirestore +import FirebaseStorage + +extension NSError { + func asFirebaseAuthError() -> FirebaseAuthError { + let authErrorCode = AuthErrorCode(_nsError: self).code + switch authErrorCode { + case .emailAlreadyInUse: + return .emailAlreadyInUse + case .userNotFound: + return .userNotFound + case .userDisabled: + return .userDisabled + case .invalidEmail, .invalidSender, .invalidRecipientEmail: + return .invalidEmail + case .networkError: + return .networkError + case .weakPassword: + return .weakPassword + case .wrongPassword: + return .wrongPassword + case .operationNotAllowed: + return .operationNotAllowed + case .keychainError: + return .keychainError + default: + return .unknownNSError(self) + } + } + + func asFirestoreError() -> FirebaseFirestoreError { + let firestoreErrorCode = FirestoreErrorCode(_nsError: self) + switch firestoreErrorCode.code { + case .cancelled: + return .cancelled + case .unknown: + return .unknownNSError(self) + case .invalidArgument: + return .invalidArgument + case .deadlineExceeded: + return .deadlineExceeded + case .notFound: + return .notFound + case .alreadyExists: + return .alreadyExists + case .permissionDenied: + return .permissionDenied + case .resourceExhausted: + return .resourceExhausted + case .failedPrecondition: + return .failedPrecondition + case .aborted: + return .aborted + case .outOfRange: + return .outOfRange + case .unimplemented: + return .unimplemented + case .internal: + return .internalError + case .unavailable: + return .unavailable + case .dataLoss: + return .dataLoss + case .unauthenticated: + return .unauthenticated + default: + return .unknownNSError(self) + } + } + + func asFirebaseStorageError() -> FirebaseStorageError { + if let storageErrorCode = StorageErrorCode(rawValue: self.code) { + switch storageErrorCode { + case .retryLimitExceeded: + return .retryLimitExceeded + case .bucketNotFound: + return .bucketNotFound + case .objectNotFound: + return .objectNotFound + case .quotaExceeded: + return .quotaExceeded + case .unauthorized: + return .unauthorized + default: + return .unknownStorageError(storageErrorCode) + } + } else { + return .unknownNSError(self) + } + } +} diff --git a/FairShare/Models/AuthViewModel.swift b/FairShare/Models/AuthViewModel.swift index 373d3a4..8c3d57d 100644 --- a/FairShare/Models/AuthViewModel.swift +++ b/FairShare/Models/AuthViewModel.swift @@ -54,8 +54,10 @@ extension AuthViewModel { let result = try await AuthService.signIn(with: email, password: password) self.userSession = result try await self.fetchUser() - } catch let error as FirebaseAuthError { - self.showError(for: error.errorMessage) + } catch { + let firebaseAuthError = FirebaseAuthError(error) + self.showError(for: firebaseAuthError.errorMessage) + print("Error signing in user: \(firebaseAuthError.errorMessage)") } } @@ -64,8 +66,10 @@ extension AuthViewModel { try await AuthService.signOut() self.userSession = nil self.currentUser = nil - } catch let error as FirebaseAuthError { - self.showError(for: error.errorMessage) + } catch { + let firebaseAuthError = FirebaseAuthError(error) + self.showError(for: firebaseAuthError.errorMessage) + print("Error signing out user: \(firebaseAuthError.errorMessage)") } } @@ -81,8 +85,11 @@ extension AuthViewModel { try await DBService.createUser(from: user) try await self.fetchUser() } catch { - print("Error creating user: \(error)") - throw error + let firebaseAuthError = FirebaseAuthError(error) + self.showError(for: firebaseAuthError.errorMessage) + print("Error creating user: \(firebaseAuthError.errorMessage)") + + throw firebaseAuthError } } @@ -92,26 +99,28 @@ extension AuthViewModel { private func fetchUser() async throws { guard let userId = AuthService.CurrentUser?.uid else { - print("Error: User ID is nil.") - return + throw FirebaseAuthError.userNotFound } self.isLoading = true do { - let fetchedUser = try await DBService.fetchUser(userId: userId) - self.currentUser = fetchedUser + self.currentUser = try await DBService.fetchUser(userId: userId) try await self.fetchUserReceipts() try await self.fetchContacts() } catch { - print("Error fetching user: \(error)") - throw error + let firebaseAuthError = FirebaseAuthError(error) + self.showError(for: firebaseAuthError.errorMessage) + print("Error fetching user: \(firebaseAuthError.errorMessage)") + + throw firebaseAuthError } } } ///Receipt extension AuthViewModel { + //TODO: Update error handling func fetchUserReceipts() async throws { guard let userID = self.currentUser?.id else { print("Error: Current user ID is nil.") @@ -143,6 +152,7 @@ extension AuthViewModel { ///Contacts extension AuthViewModel { + //TODO: Update error handling func addContacts(contacts: [ContactModel]) async throws { do { for contact in contacts { diff --git a/FairShare/Networking/AuthService.swift b/FairShare/Networking/AuthService.swift index 36d13f0..568a846 100644 --- a/FairShare/Networking/AuthService.swift +++ b/FairShare/Networking/AuthService.swift @@ -26,18 +26,16 @@ extension AuthService { do { let result = try await Authentication.signIn(withEmail: email, password: password) return result.user - } catch let error as NSError { - print("Error signing in user: \(FirebaseAuthError(error).errorMessage)") - throw FirebaseAuthError(error) + } catch { + throw error } } static func signOut() async throws { do { try Authentication.signOut() - } catch let error as NSError { - print("Error signing out user: \(FirebaseAuthError(error).errorMessage)") - throw FirebaseAuthError(error) + } catch { + throw error } } @@ -46,7 +44,6 @@ extension AuthService { let results = try await Authentication.createUser(withEmail: withEmail, password: password) return results } catch { - print("Error creating user: \(error)") throw error } } From 9ea74e2d941eddc50ac44ddbf661afa65fc0a27d Mon Sep 17 00:00:00 2001 From: Stephanie Ramirez | Fueled Date: Sat, 9 Nov 2024 20:02:35 -0500 Subject: [PATCH 2/5] temp --- FairShare.xcodeproj/project.pbxproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/FairShare.xcodeproj/project.pbxproj b/FairShare.xcodeproj/project.pbxproj index 0892f23..82ea466 100644 --- a/FairShare.xcodeproj/project.pbxproj +++ b/FairShare.xcodeproj/project.pbxproj @@ -52,9 +52,9 @@ 909E699C2C02C820009D00D6 /* Image+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909E699B2C02C820009D00D6 /* Image+Helper.swift */; }; 909E699E2C02C82A009D00D6 /* Color+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909E699D2C02C82A009D00D6 /* Color+Helper.swift */; }; 909E69A02C02C865009D00D6 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909E699F2C02C865009D00D6 /* Constants.swift */; }; + 90B98CC12C434443006B6B83 /* NSError+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B98CC02C434443006B6B83 /* NSError+Helper.swift */; }; 90B98CC32C437B94006B6B83 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B98CC22C437B94006B6B83 /* MessageView.swift */; }; 90B98CC52C439451006B6B83 /* String+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B98CC42C439451006B6B83 /* String+Helper.swift */; }; - 90B98CC12C434443006B6B83 /* NSError+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B98CC02C434443006B6B83 /* NSError+Helper.swift */; }; 90BD783A2C0AEEB300C6A889 /* ReceiptItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90BD78392C0AEEB300C6A889 /* ReceiptItemsView.swift */; }; 90BD783C2C0C051E00C6A889 /* EditableReceiptItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90BD783B2C0C051E00C6A889 /* EditableReceiptItemView.swift */; }; 90CA9FB92C396E11009F1251 /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CA9FB82C396E11009F1251 /* SideMenuView.swift */; }; @@ -120,9 +120,9 @@ 909E699B2C02C820009D00D6 /* Image+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image+Helper.swift"; sourceTree = ""; }; 909E699D2C02C82A009D00D6 /* Color+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Helper.swift"; sourceTree = ""; }; 909E699F2C02C865009D00D6 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 90B98CC02C434443006B6B83 /* NSError+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Helper.swift"; sourceTree = ""; }; 90B98CC22C437B94006B6B83 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 90B98CC42C439451006B6B83 /* String+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Helper.swift"; sourceTree = ""; }; - 90B98CC02C434443006B6B83 /* NSError+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Helper.swift"; sourceTree = ""; }; 90BD78392C0AEEB300C6A889 /* ReceiptItemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptItemsView.swift; sourceTree = ""; }; 90BD783B2C0C051E00C6A889 /* EditableReceiptItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableReceiptItemView.swift; sourceTree = ""; }; 90CA9FB82C396E11009F1251 /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = ""; }; @@ -736,6 +736,7 @@ DEVELOPMENT_TEAM = FSUP887MQU; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; INFOPLIST_KEY_NSCameraUsageDescription = "This app needs to capture images of your receipt"; INFOPLIST_KEY_NSContactsUsageDescription = "Allow Contact Access"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -768,6 +769,7 @@ DEVELOPMENT_TEAM = FSUP887MQU; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; INFOPLIST_KEY_NSCameraUsageDescription = "This app needs to capture images of your receipt"; INFOPLIST_KEY_NSContactsUsageDescription = "Allow Contact Access"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; From 9a16d3f7d840758ddc87ac9f4aede2972d0c0f19 Mon Sep 17 00:00:00 2001 From: Stephanie Ramirez | Fueled Date: Sat, 9 Nov 2024 21:03:44 -0500 Subject: [PATCH 3/5] temp 2 --- FairShare/Models/AuthViewModel.swift | 1 + FairShare/Persistence.swift | 97 +++++++++++++++------------- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/FairShare/Models/AuthViewModel.swift b/FairShare/Models/AuthViewModel.swift index 8c3d57d..1008617 100644 --- a/FairShare/Models/AuthViewModel.swift +++ b/FairShare/Models/AuthViewModel.swift @@ -11,6 +11,7 @@ import FirebaseFirestoreSwift @MainActor class AuthViewModel: ObservableObject { + //TODO: 1) Make sidebar contact reusable enum(3) profile(view, edit, delete), receipt items(select), new receipt guests(select). shared items are search bar and add new contact. 2) Make core data contact, 3) Remove contact info from firebase @Published var userSession: FirebaseAuth.User? @Published var currentUser: UserModel? @Published var receipts: [ReceiptModel] = [] diff --git a/FairShare/Persistence.swift b/FairShare/Persistence.swift index eb5e50c..b75f602 100644 --- a/FairShare/Persistence.swift +++ b/FairShare/Persistence.swift @@ -8,49 +8,56 @@ import CoreData struct PersistenceController { - static let shared = PersistenceController() - - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - for _ in 0..<10 { - let newItem = Item(context: viewContext) - newItem.timestamp = Date() - } - do { - try viewContext.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 result - }() - - let container: NSPersistentContainer - - init(inMemory: Bool = false) { - container = NSPersistentContainer(name: "FairShare") - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - // 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. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - container.viewContext.automaticallyMergesChangesFromParent = true - } + static let shared = PersistenceController() + + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + let viewContext = result.container.viewContext + + for _ in 0..<10 { + let newItem = Item(context: viewContext) + newItem.timestamp = Date() + } + + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + + return result + }() + + let container: NSPersistentContainer + var context: NSManagedObjectContext { container.viewContext } + + init(inMemory: Bool = false) { + container = NSPersistentContainer(name: "FairShare") + + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + + container.viewContext.automaticallyMergesChangesFromParent = true + container.loadPersistentStores( + completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + } + ) + + container.viewContext.automaticallyMergesChangesFromParent = true + } + + func saveContext() { + if context.hasChanges { + do { + try context.save() + } catch let error as NSError { + NSLog("Unresolved error saving context: \(error), \(error.userInfo)") + } + } + } } From 835b8073737ee9bd67fb5f37b0d0b13cd9d745ec Mon Sep 17 00:00:00 2001 From: Stephanie Ramirez | Fueled Date: Sat, 9 Nov 2024 23:05:16 -0500 Subject: [PATCH 4/5] temp 3 --- FairShare.xcodeproj/project.pbxproj | 18 ++++++++- .../CoreData/ContactData+CoreDataClass.swift | 14 +++++++ .../ContactData+CoreDataProperties.swift | 27 ++++++++++++++ FairShare/{ => CoreData}/Persistence.swift | 37 +++++++++++++++++++ .../FairShare.xcdatamodel/contents | 11 ++++-- FairShare/Models/ContactModel.swift | 7 ++++ 6 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 FairShare/CoreData/ContactData+CoreDataClass.swift create mode 100644 FairShare/CoreData/ContactData+CoreDataProperties.swift rename FairShare/{ => CoreData}/Persistence.swift (61%) diff --git a/FairShare.xcodeproj/project.pbxproj b/FairShare.xcodeproj/project.pbxproj index 82ea466..1c42d66 100644 --- a/FairShare.xcodeproj/project.pbxproj +++ b/FairShare.xcodeproj/project.pbxproj @@ -39,6 +39,8 @@ 90676E962C3A5C2600828276 /* ReceiptDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90676E952C3A5C2600828276 /* ReceiptDetailViewModel.swift */; }; 90676E982C3A5CDA00828276 /* Double+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90676E972C3A5CDA00828276 /* Double+Helper.swift */; }; 90676E9A2C3A66ED00828276 /* ReceiptTextModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90676E992C3A66ED00828276 /* ReceiptTextModel.swift */; }; + 906F97DC2CE0622B0023B7C1 /* ContactData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906F97DA2CE0622B0023B7C1 /* ContactData+CoreDataClass.swift */; }; + 906F97DD2CE0622B0023B7C1 /* ContactData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906F97DB2CE0622B0023B7C1 /* ContactData+CoreDataProperties.swift */; }; 9072AFC52BE9C46E00DE6FEB /* FairShareApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9072AFC42BE9C46E00DE6FEB /* FairShareApp.swift */; }; 9072AFC72BE9C46E00DE6FEB /* EntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9072AFC62BE9C46E00DE6FEB /* EntryView.swift */; }; 9072AFC92BE9C47000DE6FEB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9072AFC82BE9C47000DE6FEB /* Assets.xcassets */; }; @@ -104,6 +106,8 @@ 90676E952C3A5C2600828276 /* ReceiptDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptDetailViewModel.swift; sourceTree = ""; }; 90676E972C3A5CDA00828276 /* Double+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Helper.swift"; sourceTree = ""; }; 90676E992C3A66ED00828276 /* ReceiptTextModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptTextModel.swift; sourceTree = ""; }; + 906F97DA2CE0622B0023B7C1 /* ContactData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactData+CoreDataClass.swift"; sourceTree = ""; }; + 906F97DB2CE0622B0023B7C1 /* ContactData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactData+CoreDataProperties.swift"; sourceTree = ""; }; 9072AFC12BE9C46E00DE6FEB /* FairShare.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FairShare.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9072AFC42BE9C46E00DE6FEB /* FairShareApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FairShareApp.swift; sourceTree = ""; }; 9072AFC62BE9C46E00DE6FEB /* EntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryView.swift; sourceTree = ""; }; @@ -195,6 +199,16 @@ path = Elements; sourceTree = ""; }; + 906F97DE2CE063760023B7C1 /* CoreData */ = { + isa = PBXGroup; + children = ( + 906F97DA2CE0622B0023B7C1 /* ContactData+CoreDataClass.swift */, + 906F97DB2CE0622B0023B7C1 /* ContactData+CoreDataProperties.swift */, + 9072AFCD2BE9C47000DE6FEB /* Persistence.swift */, + ); + path = CoreData; + sourceTree = ""; + }; 9072AFB82BE9C46E00DE6FEB = { isa = PBXGroup; children = ( @@ -219,6 +233,7 @@ isa = PBXGroup; children = ( 90FB96202BF02C880061E83D /* Application */, + 906F97DE2CE063760023B7C1 /* CoreData */, 90FB96212BF02D010061E83D /* Networking */, 90FB961F2BF02C470061E83D /* Errors */, 90FB961E2BF02C120061E83D /* Helpers */, @@ -226,7 +241,6 @@ 90FB961D2BF02BF60061E83D /* Models */, 9072AFC82BE9C47000DE6FEB /* Assets.xcassets */, 902C01F52BF00B0E0004B01A /* GoogleService-Info.plist */, - 9072AFCD2BE9C47000DE6FEB /* Persistence.swift */, 9072AFCF2BE9C47000DE6FEB /* FairShare.xcdatamodeld */, 9072AFCA2BE9C47000DE6FEB /* Preview Content */, ); @@ -556,6 +570,8 @@ 9072AFC72BE9C46E00DE6FEB /* EntryView.swift in Sources */, 90B98CC12C434443006B6B83 /* NSError+Helper.swift in Sources */, 90F4761F2C154FE20075B36B /* AuthService.swift in Sources */, + 906F97DD2CE0622B0023B7C1 /* ContactData+CoreDataProperties.swift in Sources */, + 906F97DC2CE0622B0023B7C1 /* ContactData+CoreDataClass.swift in Sources */, 90FB96172BF023340061E83D /* Date+Helper.swift in Sources */, 909E69942C02A5C7009D00D6 /* CameraView.swift in Sources */, 90B98CC52C439451006B6B83 /* String+Helper.swift in Sources */, diff --git a/FairShare/CoreData/ContactData+CoreDataClass.swift b/FairShare/CoreData/ContactData+CoreDataClass.swift new file mode 100644 index 0000000..717bf65 --- /dev/null +++ b/FairShare/CoreData/ContactData+CoreDataClass.swift @@ -0,0 +1,14 @@ +// +// ContactData+CoreDataClass.swift +// FairShare +// +// Created by Stephanie Ramirez on 11/9/24. +// +// + +import Foundation +import CoreData + +public class ContactData: NSManagedObject { + +} diff --git a/FairShare/CoreData/ContactData+CoreDataProperties.swift b/FairShare/CoreData/ContactData+CoreDataProperties.swift new file mode 100644 index 0000000..099e61a --- /dev/null +++ b/FairShare/CoreData/ContactData+CoreDataProperties.swift @@ -0,0 +1,27 @@ +// +// ContactData+CoreDataProperties.swift +// FairShare +// +// Created by Stephanie Ramirez on 11/9/24. +// +// + +import Foundation +import CoreData + +extension ContactData { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "ContactData") + } + + @NSManaged public var id: String + @NSManaged public var firstName: String + @NSManaged public var lastName: String + @NSManaged public var phoneNumber: String? + +} + +extension ContactData : Identifiable { + +} diff --git a/FairShare/Persistence.swift b/FairShare/CoreData/Persistence.swift similarity index 61% rename from FairShare/Persistence.swift rename to FairShare/CoreData/Persistence.swift index b75f602..f0d65ea 100644 --- a/FairShare/Persistence.swift +++ b/FairShare/CoreData/Persistence.swift @@ -19,6 +19,14 @@ struct PersistenceController { newItem.timestamp = Date() } + for _ in 0..<10 { + let newContact = ContactData(context: viewContext) + newContact.id = ContactModel.dummyData.id + newContact.firstName = ContactModel.dummyData.firstName + newContact.lastName = ContactModel.dummyData.lastName + newContact.phoneNumber = ContactModel.dummyData.phoneNumber + } + do { try viewContext.save() } catch { @@ -61,3 +69,32 @@ struct PersistenceController { } } } + +///Contacts +extension PersistenceController { + func fetchAllContacts() -> [ContactData] { + let request = NSFetchRequest(entityName: "ContactData") + + do { + return try context.fetch(request) + } catch { + return [] + } + } + + func addContact(contact: ContactModel) { + let newContact = ContactData(context: context) + newContact.id = contact.id + newContact.firstName = contact.firstName + newContact.lastName = contact.lastName + newContact.phoneNumber = contact.phoneNumber + + saveContext() + } + + func deleteContact(_ contactData: ContactData) { + context.delete(contactData) + + saveContext() + } +} diff --git a/FairShare/FairShare.xcdatamodeld/FairShare.xcdatamodel/contents b/FairShare/FairShare.xcdatamodeld/FairShare.xcdatamodel/contents index 9ed2921..5d31498 100644 --- a/FairShare/FairShare.xcdatamodeld/FairShare.xcdatamodel/contents +++ b/FairShare/FairShare.xcdatamodeld/FairShare.xcdatamodel/contents @@ -1,9 +1,12 @@ - + + + + + + + - - - \ No newline at end of file diff --git a/FairShare/Models/ContactModel.swift b/FairShare/Models/ContactModel.swift index ac1173e..5feabc3 100644 --- a/FairShare/Models/ContactModel.swift +++ b/FairShare/Models/ContactModel.swift @@ -20,6 +20,13 @@ struct ContactModel: PayerProtocol { self.phoneNumber = phoneNumber } + init(_ contactData: ContactData) { + self.id = contactData.id + self.firstName = contactData.firstName + self.lastName = contactData.lastName + self.phoneNumber = contactData.phoneNumber ?? "" + } + //TODO: active user can create "guest" contact by phone number. If this contact makes an account later, they can be linked to their guest account and updated via matching phone number. static let dummyData: ContactModel = dummyArrayData[0] From 05d080ec397b0da8bf4d0c90d065b0b126f4ca1a Mon Sep 17 00:00:00 2001 From: Stephanie Ramirez | Fueled Date: Mon, 11 Nov 2024 14:26:05 -0500 Subject: [PATCH 5/5] temp 4 --- FairShare.xcodeproj/project.pbxproj | 4 + .../IDEFindNavigatorScopes.plist | 5 + FairShare/Application/FairShareApp.swift | 3 + FairShare/CoreData/Persistence.swift | 10 +- FairShare/Helpers/Constants.swift | 5 + FairShare/Models/AuthViewModel.swift | 21 +-- FairShare/Models/ContactModel.swift | 2 - FairShare/UI/Profile/ContactListView.swift | 162 ++++++++++++++++++ FairShare/UI/Profile/ProfileView.swift | 89 +++++----- 9 files changed, 234 insertions(+), 67 deletions(-) create mode 100644 FairShare.xcodeproj/project.xcworkspace/xcuserdata/stephanie.xcuserdatad/IDEFindNavigatorScopes.plist create mode 100644 FairShare/UI/Profile/ContactListView.swift diff --git a/FairShare.xcodeproj/project.pbxproj b/FairShare.xcodeproj/project.pbxproj index 1c42d66..4adf11f 100644 --- a/FairShare.xcodeproj/project.pbxproj +++ b/FairShare.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ 90676E9A2C3A66ED00828276 /* ReceiptTextModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90676E992C3A66ED00828276 /* ReceiptTextModel.swift */; }; 906F97DC2CE0622B0023B7C1 /* ContactData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906F97DA2CE0622B0023B7C1 /* ContactData+CoreDataClass.swift */; }; 906F97DD2CE0622B0023B7C1 /* ContactData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906F97DB2CE0622B0023B7C1 /* ContactData+CoreDataProperties.swift */; }; + 906F97E02CE0695C0023B7C1 /* ContactListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906F97DF2CE0695C0023B7C1 /* ContactListView.swift */; }; 9072AFC52BE9C46E00DE6FEB /* FairShareApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9072AFC42BE9C46E00DE6FEB /* FairShareApp.swift */; }; 9072AFC72BE9C46E00DE6FEB /* EntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9072AFC62BE9C46E00DE6FEB /* EntryView.swift */; }; 9072AFC92BE9C47000DE6FEB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9072AFC82BE9C47000DE6FEB /* Assets.xcassets */; }; @@ -108,6 +109,7 @@ 90676E992C3A66ED00828276 /* ReceiptTextModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptTextModel.swift; sourceTree = ""; }; 906F97DA2CE0622B0023B7C1 /* ContactData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactData+CoreDataClass.swift"; sourceTree = ""; }; 906F97DB2CE0622B0023B7C1 /* ContactData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactData+CoreDataProperties.swift"; sourceTree = ""; }; + 906F97DF2CE0695C0023B7C1 /* ContactListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListView.swift; sourceTree = ""; }; 9072AFC12BE9C46E00DE6FEB /* FairShare.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FairShare.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9072AFC42BE9C46E00DE6FEB /* FairShareApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FairShareApp.swift; sourceTree = ""; }; 9072AFC62BE9C46E00DE6FEB /* EntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryView.swift; sourceTree = ""; }; @@ -276,6 +278,7 @@ isa = PBXGroup; children = ( 90FB962C2BF0753E0061E83D /* ProfileView.swift */, + 906F97DF2CE0695C0023B7C1 /* ContactListView.swift */, 9029DF132C3140A2008586E6 /* ContactPickerView.swift */, 90FB962E2BF0779B0061E83D /* SettingsRowView.swift */, ); @@ -577,6 +580,7 @@ 90B98CC52C439451006B6B83 /* String+Helper.swift in Sources */, 909E69A02C02C865009D00D6 /* Constants.swift in Sources */, 90676E982C3A5CDA00828276 /* Double+Helper.swift in Sources */, + 906F97E02CE0695C0023B7C1 /* ContactListView.swift in Sources */, 9072AFC52BE9C46E00DE6FEB /* FairShareApp.swift in Sources */, 902C01FA2BF017660004B01A /* ReceiptView.swift in Sources */, 902D77022C27766800F7B462 /* ReceiptDetailView.swift in Sources */, diff --git a/FairShare.xcodeproj/project.xcworkspace/xcuserdata/stephanie.xcuserdatad/IDEFindNavigatorScopes.plist b/FairShare.xcodeproj/project.xcworkspace/xcuserdata/stephanie.xcuserdatad/IDEFindNavigatorScopes.plist new file mode 100644 index 0000000..5dd5da8 --- /dev/null +++ b/FairShare.xcodeproj/project.xcworkspace/xcuserdata/stephanie.xcuserdatad/IDEFindNavigatorScopes.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/FairShare/Application/FairShareApp.swift b/FairShare/Application/FairShareApp.swift index 60decbf..0ddae6b 100644 --- a/FairShare/Application/FairShareApp.swift +++ b/FairShare/Application/FairShareApp.swift @@ -22,9 +22,12 @@ struct FairShareApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate @StateObject var viewModel = AuthViewModel() + let persistenceController = PersistenceController.shared + var body: some Scene { WindowGroup { EntryView() + .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(viewModel) } } diff --git a/FairShare/CoreData/Persistence.swift b/FairShare/CoreData/Persistence.swift index f0d65ea..5a252af 100644 --- a/FairShare/CoreData/Persistence.swift +++ b/FairShare/CoreData/Persistence.swift @@ -19,12 +19,12 @@ struct PersistenceController { newItem.timestamp = Date() } - for _ in 0..<10 { + for contact in ContactModel.dummyArrayData { let newContact = ContactData(context: viewContext) - newContact.id = ContactModel.dummyData.id - newContact.firstName = ContactModel.dummyData.firstName - newContact.lastName = ContactModel.dummyData.lastName - newContact.phoneNumber = ContactModel.dummyData.phoneNumber + newContact.id = contact.id + newContact.firstName = contact.firstName + newContact.lastName = contact.lastName + newContact.phoneNumber = contact.phoneNumber } do { diff --git a/FairShare/Helpers/Constants.swift b/FairShare/Helpers/Constants.swift index e073ebb..f124df2 100644 --- a/FairShare/Helpers/Constants.swift +++ b/FairShare/Helpers/Constants.swift @@ -49,6 +49,11 @@ struct Images { } struct Strings { + struct ContactListView { + static let navigationTitle = TextAsset(string: "Contacts") + static let emptyState = TextAsset(string: "Click the + to add a new contact") + } + struct LoginView { static let confirmString = "OK" static let welcome = "Welcome Back" diff --git a/FairShare/Models/AuthViewModel.swift b/FairShare/Models/AuthViewModel.swift index 1008617..975557d 100644 --- a/FairShare/Models/AuthViewModel.swift +++ b/FairShare/Models/AuthViewModel.swift @@ -11,7 +11,7 @@ import FirebaseFirestoreSwift @MainActor class AuthViewModel: ObservableObject { - //TODO: 1) Make sidebar contact reusable enum(3) profile(view, edit, delete), receipt items(select), new receipt guests(select). shared items are search bar and add new contact. 2) Make core data contact, 3) Remove contact info from firebase + //TODO: 1) Make sidebar contact reusable enum(3) profile(view, edit, delete- via swipe), receipt items(select), new receipt guests(select). shared items are search bar and add new contact. 2) Make core data contact, 3) Remove contact info from firebase and old model. @Published var userSession: FirebaseAuth.User? @Published var currentUser: UserModel? @Published var receipts: [ReceiptModel] = [] @@ -153,16 +153,17 @@ extension AuthViewModel { ///Contacts extension AuthViewModel { - //TODO: Update error handling func addContacts(contacts: [ContactModel]) async throws { - do { - for contact in contacts { - try await DBService.addContact(contact: contact, creatorID: self.currentUser!.id) - } - } catch { - print("Error writing document: \(error)") - throw error - } +// do { +// for contact in contacts { +// try await DBService.addContact(contact: contact, creatorID: self.currentUser!.id) +// } +// } catch { +// print("Error writing document: \(error)") +// throw error +// } + + } func fetchContacts() async throws { diff --git a/FairShare/Models/ContactModel.swift b/FairShare/Models/ContactModel.swift index 5feabc3..5d1a05e 100644 --- a/FairShare/Models/ContactModel.swift +++ b/FairShare/Models/ContactModel.swift @@ -27,8 +27,6 @@ struct ContactModel: PayerProtocol { self.phoneNumber = contactData.phoneNumber ?? "" } - //TODO: active user can create "guest" contact by phone number. If this contact makes an account later, they can be linked to their guest account and updated via matching phone number. - static let dummyData: ContactModel = dummyArrayData[0] static let dummyArrayData: [ContactModel] = [ ContactModel( diff --git a/FairShare/UI/Profile/ContactListView.swift b/FairShare/UI/Profile/ContactListView.swift new file mode 100644 index 0000000..dc97c31 --- /dev/null +++ b/FairShare/UI/Profile/ContactListView.swift @@ -0,0 +1,162 @@ +// +// ContactListView.swift +// FairShare +// +// Created by Stephanie Ramirez on 11/9/24. +// + +import CoreData +import SwiftUI + +struct ContactListView: View { + @Environment(\.managedObjectContext) private var viewContext + @StateObject private var viewModel = ContactViewModel() + @State private var showContactPicker = false + @State private var selectedContacts: [ContactModel] = [] + + private var groupedContacts: [String: [ContactModel]] { + Dictionary(grouping: viewModel.contacts, by: { String($0.firstName.prefix(1)) }) + } + + private var sortedSectionKeys: [String] { + groupedContacts.keys.sorted() + } + + var body: some View { + VStack(spacing: 0) { + List { + ForEach(sortedSectionKeys, id: \.self) { key in + Section(header: Text(key)) { + ForEach(groupedContacts[key] ?? [], id: \.self) { contact in + VStack(alignment: .leading) { + Text("\(contact.firstName) \(contact.lastName)") + .font(.headline) + Text(contact.phoneNumber) + .font(.subheadline) + .foregroundColor(.gray) + } + .padding(.vertical, 4) + .swipeActions(allowsFullSwipe: false) { + Button(role: .destructive) { + print("Deleting contact \(contact.firstName)") + } label: { + Label("Delete", systemImage: "trash.fill") + } + + Button { + print("Edit \(contact.firstName)") + } label: { + Text("Edit") + } + .tint(.green) + } + } + } + } + } + .navigationTitle("Contacts") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + showContactPicker.toggle() + } label: { + Image(systemName: "plus") + .foregroundColor(.blue) + } + } + } + } + .fullScreenCover( + isPresented: $showContactPicker, + content: { + ContactPickerView(selectedContacts: $selectedContacts) + .edgesIgnoringSafeArea(.all) + } + ) + .onAppear { + viewModel.setContext(viewContext) + viewModel.fetchContacts() + } + .onChange(of: selectedContacts) { + Task { + viewModel.addContacts(selectedContacts) + } + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContactListView() + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} + +class ContactViewModel: ObservableObject { + @Published var contacts: [ContactModel] = [] + + private var context: NSManagedObjectContext? + + func setContext(_ context: NSManagedObjectContext) { + self.context = context + } + + func fetchContacts() { + guard let context = context else { return } + let fetchRequest: NSFetchRequest = ContactData.fetchRequest() + + do { + let fetchedContacts = try context.fetch(fetchRequest) + self.contacts = convertToContactModels(from: fetchedContacts) + + } catch { + print("Failed to fetch contacts: \(error.localizedDescription)") + } + } + + func addContacts(_ contacts: [ContactModel]) { + guard let context = context else { + return + } + + for contact in contacts { + let newContact = ContactData(context: context) + newContact.id = contact.id + newContact.firstName = contact.firstName + newContact.lastName = contact.lastName + newContact.phoneNumber = contact.phoneNumber + } + + saveContext() + fetchContacts() + } + + private func saveContext() { + guard let context = context else { + return + } + + do { + try context.save() + } catch { + print("Failed to save context: \(error.localizedDescription)") + } + } + +//TODO: replace ContactModel with ContactData to allow for smooth deletion + +// func deleteContact(_ contact: ContactData) { +// guard let context = context else { +// return +// } +// +// context.delete(contact) +// saveContext() +// fetchContacts() +// } + + func convertToContactModels(from contactDataArray: [ContactData]) -> [ContactModel] { + return contactDataArray.map { ContactModel($0) } + } +} diff --git a/FairShare/UI/Profile/ProfileView.swift b/FairShare/UI/Profile/ProfileView.swift index 9e6092c..9e74b26 100644 --- a/FairShare/UI/Profile/ProfileView.swift +++ b/FairShare/UI/Profile/ProfileView.swift @@ -8,72 +8,61 @@ import SwiftUI struct ProfileView: View { + @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject var viewModel: AuthViewModel - @State private var showContactPicker = false @State private var selectedContacts: [ContactModel] = [] var body: some View { - if let user = viewModel.currentUser { - List { - Section { - HStack { - Text(user.initials) - .font(.title) - .fontWeight(.semibold) - .foregroundStyle(.white) - .frame(width: 72, height: 72) - .background(Color(.systemGray3)) - .clipShape(Circle()) - - VStack(alignment: .leading, spacing: 5) { - Text(user.fullName) - .font(.subheadline) + NavigationView { + if let user = viewModel.currentUser { + List { + Section { + HStack { + Text(user.initials) + .font(.title) .fontWeight(.semibold) - .padding(.top, 5) + .foregroundStyle(.white) + .frame(width: 72, height: 72) + .background(Color(.systemGray3)) + .clipShape(Circle()) + + VStack(alignment: .leading, spacing: 5) { + Text(user.fullName) + .font(.subheadline) + .fontWeight(.semibold) + .padding(.top, 5) - Text(user.email) - .font(.footnote) - .foregroundStyle(.gray) + Text(user.email) + .font(.footnote) + .foregroundStyle(.gray) + } } } - } - Section(Strings.ProfileView.account) { - Button { - Task { - try await viewModel.signOut() + Section(Strings.ProfileView.account) { + Button { + Task { + try await viewModel.signOut() + } + } label: { + SettingsRowView(rowType: .signOut) } - } label: { - SettingsRowView(rowType: .signOut) - } - Button { - print(Strings.ProfileView.delete) - } label: { - SettingsRowView(rowType: .delete) + Button { + print(Strings.ProfileView.delete) + } label: { + SettingsRowView(rowType: .delete) + } } - } - Section(Strings.ProfileView.contacts) { - //TODO: add view Contacts to allow for edits. - Button { - showContactPicker.toggle() - } label: { - SettingsRowView(rowType: .contacts) + Section(Strings.ProfileView.contacts) { + NavigationLink(destination: ContactListView() + .environment(\.managedObjectContext, viewContext)) { + SettingsRowView(rowType: .contacts) + } } } } - .fullScreenCover( - isPresented: $showContactPicker, - content: { - ContactPickerView(selectedContacts: $selectedContacts) - } - ) - .onChange(of: selectedContacts) { - Task { - try await viewModel.addContacts(contacts: selectedContacts) - } - } } } }