diff --git a/OCKSample.xcodeproj/project.pbxproj b/OCKSample.xcodeproj/project.pbxproj index a2d1387..93c65ec 100644 --- a/OCKSample.xcodeproj/project.pbxproj +++ b/OCKSample.xcodeproj/project.pbxproj @@ -19,6 +19,12 @@ 701230EA280DE687003F5ECE /* OCKAnyEvent+Answer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 701230E9280DE687003F5ECE /* OCKAnyEvent+Answer.swift */; }; 701230EB280DE687003F5ECE /* OCKAnyEvent+Answer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 701230E9280DE687003F5ECE /* OCKAnyEvent+Answer.swift */; }; 701230F12810894D003F5ECE /* CustomContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 701230F02810894D003F5ECE /* CustomContactViewController.swift */; }; + 701230FD28122A22003F5ECE /* MyContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 701230FC28122A22003F5ECE /* MyContactView.swift */; }; + 7012310028122A4E003F5ECE /* MyContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 701230FF28122A4E003F5ECE /* MyContactViewModel.swift */; }; + 70123105281255BD003F5ECE /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70123104281255BD003F5ECE /* ImagePicker.swift */; }; + 70123107281262FF003F5ECE /* OCKBiologicalSex+Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70123106281262FF003F5ECE /* OCKBiologicalSex+Hashable.swift */; }; + 701231092812DC3B003F5ECE /* MyContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 701231082812DC3B003F5ECE /* MyContactViewController.swift */; }; + 7012310B2812E24F003F5ECE /* ContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7012310A2812E24F003F5ECE /* ContactViewModel.swift */; }; 70202EC12807333900CF73FB /* CareKit in Frameworks */ = {isa = PBXBuildFile; productRef = 70202EC02807333900CF73FB /* CareKit */; }; 70202EC32807333900CF73FB /* CareKitFHIR in Frameworks */ = {isa = PBXBuildFile; productRef = 70202EC22807333900CF73FB /* CareKitFHIR */; }; 70202EC52807333A00CF73FB /* CareKitUI in Frameworks */ = {isa = PBXBuildFile; productRef = 70202EC42807333A00CF73FB /* CareKitUI */; }; @@ -201,6 +207,12 @@ 701230E7280DE595003F5ECE /* SurveyViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyViewSynchronizer.swift; sourceTree = ""; }; 701230E9280DE687003F5ECE /* OCKAnyEvent+Answer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKAnyEvent+Answer.swift"; sourceTree = ""; }; 701230F02810894D003F5ECE /* CustomContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomContactViewController.swift; sourceTree = ""; }; + 701230FC28122A22003F5ECE /* MyContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyContactView.swift; sourceTree = ""; }; + 701230FF28122A4E003F5ECE /* MyContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyContactViewModel.swift; sourceTree = ""; }; + 70123104281255BD003F5ECE /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; + 70123106281262FF003F5ECE /* OCKBiologicalSex+Hashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKBiologicalSex+Hashable.swift"; sourceTree = ""; }; + 701231082812DC3B003F5ECE /* MyContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyContactViewController.swift; sourceTree = ""; }; + 7012310A2812E24F003F5ECE /* ContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewModel.swift; sourceTree = ""; }; 70202EC9280746E900CF73FB /* ResearchKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ResearchKit.xcodeproj; path = ResearchKit/ResearchKit.xcodeproj; sourceTree = ""; }; 70202ED52807529900CF73FB /* Surveys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Surveys.swift; sourceTree = ""; }; 70308885258273D400FFABB6 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; @@ -343,6 +355,16 @@ path = Cards; sourceTree = ""; }; + 701230FE28122A31003F5ECE /* MyContact */ = { + isa = PBXGroup; + children = ( + 701230FC28122A22003F5ECE /* MyContactView.swift */, + 701231082812DC3B003F5ECE /* MyContactViewController.swift */, + 701230FF28122A4E003F5ECE /* MyContactViewModel.swift */, + ); + path = MyContact; + sourceTree = ""; + }; 70202ECA280746E900CF73FB /* Products */ = { isa = PBXGroup; children = ( @@ -355,11 +377,11 @@ 703088772582727500FFABB6 /* MainTabs */ = { isa = PBXGroup; children = ( - 91693819271B64CB00A634ED /* Stylers */, - 70308878258272CD00FFABB6 /* Care */, 7036E4D2256EBE35006E9A3C /* MainView.swift */, + 70308878258272CD00FFABB6 /* Care */, 7030887D258272F400FFABB6 /* Contact */, 7030887C258272E700FFABB6 /* Profile */, + 91693819271B64CB00A634ED /* Stylers */, ); path = MainTabs; sourceTree = ""; @@ -367,12 +389,12 @@ 70308878258272CD00FFABB6 /* Care */ = { isa = PBXGroup; children = ( + 7036E4C3256E0A48006E9A3C /* CareView.swift */, E7440E4E229477F7007AD30A /* CareViewController.swift */, + 70F03ABA2789071400E5AFB4 /* CareViewModel.swift */, + 70123068280830A1003F5ECE /* Consent.swift */, 701230E6280DE574003F5ECE /* Cards */, 701230DC280DDF3A003F5ECE /* Surveys */, - 70123068280830A1003F5ECE /* Consent.swift */, - 7036E4C3256E0A48006E9A3C /* CareView.swift */, - 70F03ABA2789071400E5AFB4 /* CareViewModel.swift */, ); path = Care; sourceTree = ""; @@ -380,8 +402,10 @@ 7030887C258272E700FFABB6 /* Profile */ = { isa = PBXGroup; children = ( + 70123104281255BD003F5ECE /* ImagePicker.swift */, 7036E4C8256E1A6F006E9A3C /* ProfileView.swift */, 7036E516256F2413006E9A3C /* ProfileViewModel.swift */, + 701230FE28122A31003F5ECE /* MyContact */, ); path = Profile; sourceTree = ""; @@ -390,6 +414,7 @@ isa = PBXGroup; children = ( 7036E4CD256E9A0C006E9A3C /* ContactView.swift */, + 7012310A2812E24F003F5ECE /* ContactViewModel.swift */, 701230F02810894D003F5ECE /* CustomContactViewController.swift */, ); path = Contact; @@ -484,15 +509,16 @@ children = ( 918FDEBB271B4E4A0045A0EF /* Calendar+Dates.swift */, 918FDEB6271B41FF0045A0EF /* Logger.swift */, - 70F03A922786087800E5AFB4 /* OCKAnyEvent+CustomStringConvertable.swift */, 701230E9280DE687003F5ECE /* OCKAnyEvent+Answer.swift */, + 70F03A922786087800E5AFB4 /* OCKAnyEvent+CustomStringConvertable.swift */, 70F03A942786093B00E5AFB4 /* OCKHealthKitPassthroughStore+Default.swift */, - 70F03A962786098F00E5AFB4 /* OCKStore+Default.swift */, - 70F03A9827860A0800E5AFB4 /* OCKSynchronizedStoreManager+Publishers.swift */, - 70F03A9E27860A8800E5AFB4 /* OCKOutcomeValue+Identifiable.swift */, 70F03AA027860AB700E5AFB4 /* OCKOutcome+default.swift */, + 70F03A9E27860A8800E5AFB4 /* OCKOutcomeValue+Identifiable.swift */, 70F03AA227860AFF00E5AFB4 /* OCKPatient+Parse.swift */, + 70F03A962786098F00E5AFB4 /* OCKStore+Default.swift */, + 70F03A9827860A0800E5AFB4 /* OCKSynchronizedStoreManager+Publishers.swift */, 7083A855279CA40A00B3832E /* PCKUtility+extention.swift */, + 70123106281262FF003F5ECE /* OCKBiologicalSex+Hashable.swift */, ); path = Extensions; sourceTree = ""; @@ -884,6 +910,7 @@ E7440E4F229477F7007AD30A /* CareViewController.swift in Sources */, 918FDEB5271B40590045A0EF /* Installation.swift in Sources */, 70F03AB62788C80500E5AFB4 /* UserStatus.swift in Sources */, + 701230FD28122A22003F5ECE /* MyContactView.swift in Sources */, 70F921AC27CABE3000368CEC /* SessionDelegate.swift in Sources */, 7036E64025717F85006E9A3C /* Constants.swift in Sources */, 918FDEBC271B4E4A0045A0EF /* Calendar+Dates.swift in Sources */, @@ -897,19 +924,24 @@ 701230EA280DE687003F5ECE /* OCKAnyEvent+Answer.swift in Sources */, 701230F12810894D003F5ECE /* CustomContactViewController.swift in Sources */, 70202ED62807529900CF73FB /* Surveys.swift in Sources */, + 70123105281255BD003F5ECE /* ImagePicker.swift in Sources */, + 701231092812DC3B003F5ECE /* MyContactViewController.swift in Sources */, 9169381D271B650700A634ED /* ColorStyle.swift in Sources */, E72B2C0A226939E3009A9438 /* AppDelegate.swift in Sources */, 7036E4C4256E0A48006E9A3C /* CareView.swift in Sources */, 70F921B027CABED600368CEC /* LocalSyncSessionDelegate.swift in Sources */, 70123069280830A1003F5ECE /* Consent.swift in Sources */, 70F03A932786087800E5AFB4 /* OCKAnyEvent+CustomStringConvertable.swift in Sources */, + 7012310B2812E24F003F5ECE /* ContactViewModel.swift in Sources */, 918FDEC3271B4E950045A0EF /* TintColorKey.swift in Sources */, + 7012310028122A4E003F5ECE /* MyContactViewModel.swift in Sources */, 70F03AA327860AFF00E5AFB4 /* OCKPatient+Parse.swift in Sources */, 918FDEB7271B41FF0045A0EF /* Logger.swift in Sources */, 70F03A9F27860A8800E5AFB4 /* OCKOutcomeValue+Identifiable.swift in Sources */, 7036E4D3256EBE35006E9A3C /* MainView.swift in Sources */, 918FDEC5271B4EA70045A0EF /* StoreManagerKey.swift in Sources */, 91693822271B897200A634ED /* Utility.swift in Sources */, + 70123107281262FF003F5ECE /* OCKBiologicalSex+Hashable.swift in Sources */, 707CC718254DA91900116728 /* OCKLocalization.swift in Sources */, E7C37849228F887800E982D8 /* TipView.swift in Sources */, 70077597252228E900EC0EDA /* User.swift in Sources */, diff --git a/OCKSample/Extensions/Logger.swift b/OCKSample/Extensions/Logger.swift index 030bd08..5520c39 100644 --- a/OCKSample/Extensions/Logger.swift +++ b/OCKSample/Extensions/Logger.swift @@ -18,6 +18,7 @@ extension Logger { static let localSessionDelegate = Logger(subsystem: subsystem, category: "LocalSessionDelegate") static let utility = Logger(subsystem: subsystem, category: "Utility") static let contact = Logger(subsystem: subsystem, category: "Contact") + static let myContact = Logger(subsystem: subsystem, category: "MyContact") static let login = Logger(subsystem: subsystem, category: "Login") static let feed = Logger(subsystem: subsystem, category: "Feed") static let watch = Logger(subsystem: subsystem, category: "Watch") diff --git a/OCKSample/Extensions/OCKBiologicalSex+Hashable.swift b/OCKSample/Extensions/OCKBiologicalSex+Hashable.swift new file mode 100644 index 0000000..104bfda --- /dev/null +++ b/OCKSample/Extensions/OCKBiologicalSex+Hashable.swift @@ -0,0 +1,13 @@ +// +// OCKBiologicalSex+Hashable.swift +// OCKSample +// +// Created by Corey Baker on 4/22/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import CareKitStore + +// Needed to use OCKBiologicalSex in a Picker. +// Simple conformance to hashable protocol. +extension OCKBiologicalSex: Hashable { } diff --git a/OCKSample/Extensions/OCKHealthKitPassthroughStore+Default.swift b/OCKSample/Extensions/OCKHealthKitPassthroughStore+Default.swift index a7585ad..fbf3c2a 100644 --- a/OCKSample/Extensions/OCKHealthKitPassthroughStore+Default.swift +++ b/OCKSample/Extensions/OCKHealthKitPassthroughStore+Default.swift @@ -42,7 +42,7 @@ extension OCKHealthKitPassthroughStore { } } - func populateSampleData() async throws { + func populateSampleData(_ patientUUID: UUID? = nil) async throws { let schedule = OCKSchedule.dailyAtTime( hour: 8, minutes: 0, start: Date(), end: nil, text: nil, diff --git a/OCKSample/Extensions/OCKStore+Default.swift b/OCKSample/Extensions/OCKStore+Default.swift index a693649..0f8b002 100644 --- a/OCKSample/Extensions/OCKStore+Default.swift +++ b/OCKSample/Extensions/OCKStore+Default.swift @@ -16,7 +16,15 @@ import ParseCareKit extension OCKStore { - func addCarePlansIfNotPresent(_ carePlans: [OCKCarePlan]) async throws { + /** + Adds an `OCKCarePlan`*asynchronously* to `OCKStore` if it has not been added already. + + - parameter carePlans: The array of `OCKCarePlan`'s to be added to the `OCKStore`. + - parameter patientUUID: The uuid of the `OCKPatient` to tie to the `OCKCarePlan`. Defaults to nil. + - throws: An error if there was a problem adding the missing `OCKCarePlan`'s. + - note: `OCKCarePlan`'s that have an existing `id` will not be added and will not cause errors to be thrown. + */ + func addCarePlansIfNotPresent(_ carePlans: [OCKCarePlan], patientUUID: UUID? = nil) async throws { let carePlanIdsToAdd = carePlans.compactMap { $0.id } // Prepare query to see if Care Plan are already added @@ -29,7 +37,9 @@ extension OCKStore { // Check results to see if there's a missing Care Plan carePlans.forEach { potentialCarePlan in if foundCarePlans.first(where: { $0.id == potentialCarePlan.id }) == nil { - carePlanNotInStore.append(potentialCarePlan) + var mutableCarePlan = potentialCarePlan + mutableCarePlan.patientUUID = patientUUID + carePlanNotInStore.append(mutableCarePlan) } } @@ -72,7 +82,7 @@ extension OCKStore { } } - func addContactsIfNotPresent(_ contacts: [OCKContact]) async throws { + func addContactsIfNotPresent(_ contacts: [OCKContact], carePlanUUID: UUID? = nil) async throws { let contactIdsToAdd = contacts.compactMap { $0.id } // Prepare query to see if contacts are already added @@ -85,7 +95,9 @@ extension OCKStore { // Check results to see if there's a missing task contacts.forEach { potential in if foundContacts.first(where: { $0.id == potential.id }) == nil { - contactsNotInStore.append(potential) + var mutableContact = potential + mutableContact.carePlanUUID = carePlanUUID + contactsNotInStore.append(mutableContact) } } @@ -100,17 +112,13 @@ extension OCKStore { } } - func populateCarePlans() async throws { - // Get patient from local database if available - guard let uuid = ProfileViewModel.getRemoteClockUUIDAfterLoginFromLocalStorage() else { - throw AppError.remoteClockIDNotAvailable - } - let patient = try await fetchPatient(withID: uuid.uuidString) + func populateCarePlans(patientUUID: UUID? = nil) async throws { let checkInCarePlan = OCKCarePlan(id: CarePlanID.checkIn.rawValue, title: "Check in Care Plan", - patientUUID: patient.uuid) - try await addCarePlansIfNotPresent([checkInCarePlan]) + patientUUID: nil) + + try await addCarePlansIfNotPresent([checkInCarePlan], patientUUID: patientUUID) } @MainActor @@ -134,8 +142,8 @@ extension OCKStore { } // Adds tasks and contacts into the store - func populateSampleData() async throws { - try await populateCarePlans() + func populateSampleData(_ patientUUID: UUID? = nil) async throws { + try await populateCarePlans(patientUUID: patientUUID) let carePlanUUIDs = try await Self.getCarePlanUUIDs() let thisMorning = Calendar.current.startOfDay(for: Date()) diff --git a/OCKSample/Extensions/OCKSynchronizedStoreManager+Publishers.swift b/OCKSample/Extensions/OCKSynchronizedStoreManager+Publishers.swift index 69fd687..8a5ef97 100644 --- a/OCKSample/Extensions/OCKSynchronizedStoreManager+Publishers.swift +++ b/OCKSample/Extensions/OCKSynchronizedStoreManager+Publishers.swift @@ -47,6 +47,42 @@ extension OCKSynchronizedStoreManager { .prepend(presentValuePublisher)) } + // MARK: Contacts + + func contactsPublisher(categories: [OCKStoreNotificationCategory]) -> AnyPublisher { + return AnyPublisher(notificationPublisher + .compactMap { $0 as? OCKContactNotification } + .filter { categories.contains($0.category) } + .map { $0.contact }) + } + + func publisher(forContactID id: String, + categories: [OCKStoreNotificationCategory]) -> AnyPublisher { + return notificationPublisher + .compactMap { $0 as? OCKContactNotification } + .filter { $0.contact.id == id && categories.contains($0.category) } + .map { $0.contact } + .eraseToAnyPublisher() + } + + func publisher(forContact contact: OCKAnyContact, + categories: [OCKStoreNotificationCategory], + fetchImmediately: Bool = true) -> AnyPublisher { + let presentValuePublisher = Future({ completion in + self.store.fetchAnyContact(withID: contact.id) { result in + completion(.success((try? result.get()) ?? contact)) + } + }) + + let changePublisher = notificationPublisher + .compactMap { $0 as? OCKContactNotification } + .filter { $0.contact.id == contact.id && categories.contains($0.category) } + .map { $0.contact } + + // swiftlint:disable:next line_length + return fetchImmediately ? AnyPublisher(changePublisher.prepend(presentValuePublisher)) : AnyPublisher(changePublisher) + } + // MARK: Tasks func publisherForTasks(categories: [OCKStoreNotificationCategory]) -> AnyPublisher { diff --git a/OCKSample/MainTabs/Care/CareView.swift b/OCKSample/MainTabs/Care/CareView.swift index 5dab6af..029bc3b 100644 --- a/OCKSample/MainTabs/Care/CareView.swift +++ b/OCKSample/MainTabs/Care/CareView.swift @@ -18,6 +18,7 @@ struct CareView: UIViewControllerRepresentable { @ObservedObject var viewModel = CareViewModel() + @MainActor func makeUIViewController(context: Context) -> some UIViewController { let view = createCareView() diff --git a/OCKSample/MainTabs/Contact/ContactView.swift b/OCKSample/MainTabs/Contact/ContactView.swift index 328c2d7..c9866df 100644 --- a/OCKSample/MainTabs/Contact/ContactView.swift +++ b/OCKSample/MainTabs/Contact/ContactView.swift @@ -12,19 +12,50 @@ import CareKit import CareKitStore import os.log +/* + You should notice this looks like CareView but with references + to CustomContactViewController instead. +*/ + struct ContactView: UIViewControllerRepresentable { + @ObservedObject var viewModel = ContactViewModel() + + @MainActor func makeUIViewController(context: Context) -> some UIViewController { - guard let manager = StoreManagerKey.defaultValue else { - Logger.contact.error("Couldn't unwrap storeManager") - return UINavigationController() - } - let contactViewController = CustomContactViewController(storeManager: manager) - return UINavigationController(rootViewController: contactViewController) + let viewController = createViewContoller() + let navigationController = UINavigationController(rootViewController: viewController) + navigationController.navigationBar.backgroundColor = UIColor { $0.userInterfaceStyle == .light ? #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1): #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) } + + return navigationController } + @MainActor func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + // swiftlint:disable:next force_cast + let appDelegate = UIApplication.shared.delegate as! AppDelegate + if appDelegate.isFirstLogin && appDelegate.isFirstAppOpen { + guard let navigationController = uiViewController as? UINavigationController, + // swiftlint:disable:next line_length + let currentViewController = navigationController.viewControllers.first as? CustomContactViewController, + appDelegate.storeManager !== currentViewController.storeManager else { + return + } + // Replace current view controller + let viewController = createViewContoller() + navigationController.viewControllers = [viewController] + } + } + + // MARK: Helpers + func createViewContoller() -> UIViewController { + guard let manager = StoreManagerKey.defaultValue else { + Logger.contact.error("Couldn't unwrap storeManager") + return CustomContactViewController(storeManager: .init(wrapping: OCKStore(name: "none_contact", + type: .inMemory))) + } + return CustomContactViewController(storeManager: manager) } } diff --git a/OCKSample/MainTabs/Contact/ContactViewModel.swift b/OCKSample/MainTabs/Contact/ContactViewModel.swift new file mode 100644 index 0000000..afad77f --- /dev/null +++ b/OCKSample/MainTabs/Contact/ContactViewModel.swift @@ -0,0 +1,29 @@ +// +// ContactViewModel.swift +// OCKSample +// +// Created by Corey Baker on 4/22/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation + +/* + You should notice this looks like CareViewModel. +*/ + +class ContactViewModel: ObservableObject { + @Published var update = false + + init() { + NotificationCenter.default.addObserver(self, selector: #selector(reloadViewModel), + name: Notification.Name(rawValue: Constants.storeInitialized), + object: nil) + } + + // MARK: Helpers + + @objc private func reloadViewModel() { + update = !update + } +} diff --git a/OCKSample/MainTabs/Contact/CustomContactViewController.swift b/OCKSample/MainTabs/Contact/CustomContactViewController.swift index 1ead88d..e4e1d74 100644 --- a/OCKSample/MainTabs/Contact/CustomContactViewController.swift +++ b/OCKSample/MainTabs/Contact/CustomContactViewController.swift @@ -8,6 +8,7 @@ import UIKit import CareKitStore +import CareKitUI import CareKit import Contacts import ContactsUI @@ -63,6 +64,15 @@ class CustomContactViewController: OCKListViewController { } } + override func appendViewController(_ viewController: UIViewController, animated: Bool) { + super.appendViewController(viewController, animated: animated) + + // Make sure this contact card matches app style when possible + if let carekitView = viewController.view as? OCKView { + carekitView.customStyle = CustomStyleKey.defaultValue + } + } + @objc private func presentContactsListViewController() { let contactPicker = CNContactPickerViewController() @@ -76,10 +86,6 @@ class CustomContactViewController: OCKListViewController { dismiss(animated: true, completion: nil) } - func clearAndKeepSearchBar() { - clear() - } - @MainActor func fetchContacts() async throws { guard User.current != nil else { @@ -94,26 +100,25 @@ class CustomContactViewController: OCKListViewController { let contacts = try await storeManager.store.fetchAnyContacts(query: query) guard let convertedContacts = contacts as? [OCKContact], - // swiftlint:disable:next line_length - let personUUIDString = UserDefaults.standard.object(forKey: Constants.parseRemoteClockIDKey) as? String else { + let personUUIDString = ProfileViewModel.getRemoteClockUUIDAfterLoginFromLocalStorage()?.uuidString else { return } let filterdContacts = convertedContacts.filter { - // Modify this filter to not show the contact info for this user + // This filter removes the logged in users contact from the view. if $0.id == personUUIDString { return false } return true } - self.clearAndKeepSearchBar() self.allContacts = filterdContacts self.displayContacts(self.allContacts) } @MainActor func displayContacts(_ contacts: [OCKAnyContact]) { + self.clear() for contact in contacts { let contactViewController = OCKSimpleContactViewController(contact: contact, storeManager: storeManager) @@ -162,13 +167,10 @@ extension CustomContactViewController: UISearchBarDelegate { if searchBar.text!.isEmpty { // Show all contacts - clearAndKeepSearchBar() displayContacts(allContacts) return } - clearAndKeepSearchBar() - let filteredContacts = allContacts.filter { (contact: OCKAnyContact) -> Bool in if let givenName = contact.name.givenName { @@ -184,7 +186,6 @@ extension CustomContactViewController: UISearchBarDelegate { } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - clearAndKeepSearchBar() displayContacts(allContacts) } } @@ -212,8 +213,6 @@ extension CustomContactViewController: CNContactPickerDelegate { if !(self.allContacts.contains { $0.id == contactToAdd.id }) { - // Note - once the functionality is added to edit a contact, - // and let the user potentially edit before the save Task { do { _ = try await storeManager.store.addAnyContact(contactToAdd) diff --git a/OCKSample/MainTabs/MainView.swift b/OCKSample/MainTabs/MainView.swift index 186a5bb..b1b12b8 100644 --- a/OCKSample/MainTabs/MainView.swift +++ b/OCKSample/MainTabs/MainView.swift @@ -79,6 +79,7 @@ struct MainView: View { } } } + .environmentObject(profileViewModel) .environmentObject(userStatus) .statusBar(hidden: true) .accentColor(Color(tintColor)) diff --git a/OCKSample/MainTabs/Profile/ImagePicker.swift b/OCKSample/MainTabs/Profile/ImagePicker.swift new file mode 100644 index 0000000..1054ea8 --- /dev/null +++ b/OCKSample/MainTabs/Profile/ImagePicker.swift @@ -0,0 +1,50 @@ +// +// ImagePicker.swift +// OCKSample +// +// Created by Corey Baker on 4/21/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +// swiftlint:disable:next line_length +// Credit to: https://www.hackingwithswift.com/books/ios-swiftui/importing-an-image-into-swiftui-using-uiimagepickercontroller + +import SwiftUI +import UIKit + +struct ImagePicker: UIViewControllerRepresentable { + @Environment(\.presentationMode) var presentationMode + @Binding var image: UIImage? + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController { + let picker = UIImagePickerController() + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, + context: UIViewControllerRepresentableContext) { + + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { + let parent: ImagePicker + + init(_ parent: ImagePicker) { + self.parent = parent + } + + func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + if let uiImage = info[.originalImage] as? UIImage { + parent.image = uiImage + } + + parent.presentationMode.wrappedValue.dismiss() + } + } +} diff --git a/OCKSample/MainTabs/Profile/MyContact/MyContactView.swift b/OCKSample/MainTabs/Profile/MyContact/MyContactView.swift new file mode 100644 index 0000000..ee5f935 --- /dev/null +++ b/OCKSample/MainTabs/Profile/MyContact/MyContactView.swift @@ -0,0 +1,65 @@ +// +// MyContactView.swift +// OCKSample +// +// Created by Corey Baker on 4/21/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import SwiftUI +import UIKit +import CareKit +import CareKitStore +import os.log + +/* + You should notice this looks like ContactView but with references + to MyContactViewController instead. +*/ + +struct MyContactView: UIViewControllerRepresentable { + + @ObservedObject var viewModel = MyContactViewModel() + + @MainActor + func makeUIViewController(context: Context) -> some UIViewController { + let viewController = createViewContoller() + let navigationController = UINavigationController(rootViewController: viewController) + navigationController.navigationBar.backgroundColor = UIColor { $0.userInterfaceStyle == .light ? #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1): #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) } + navigationController.setNavigationBarHidden(true, animated: false) + return navigationController + } + + @MainActor + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + // swiftlint:disable:next force_cast + let appDelegate = UIApplication.shared.delegate as! AppDelegate + + if appDelegate.isFirstLogin && appDelegate.isFirstAppOpen { + guard let navigationController = uiViewController as? UINavigationController, + let currentViewController = navigationController.viewControllers.first as? MyContactViewController, + appDelegate.storeManager !== currentViewController.storeManager else { + return + } + // Replace current view controller + let viewController = createViewContoller() + navigationController.viewControllers = [viewController] + } + } + + // MARK: Helpers + func createViewContoller() -> UIViewController { + guard let manager = StoreManagerKey.defaultValue else { + Logger.myContact.error("Couldn't unwrap storeManager") + return MyContactViewController(storeManager: .init(wrapping: OCKStore(name: "none_my_contact", + type: .inMemory))) + } + return MyContactViewController(storeManager: manager) + } +} + +struct MyContactView_Previews: PreviewProvider { + static var previews: some View { + MyContactView() + } +} diff --git a/OCKSample/MainTabs/Profile/MyContact/MyContactViewController.swift b/OCKSample/MainTabs/Profile/MyContact/MyContactViewController.swift new file mode 100644 index 0000000..464c486 --- /dev/null +++ b/OCKSample/MainTabs/Profile/MyContact/MyContactViewController.swift @@ -0,0 +1,114 @@ +// +// MyContactViewController.swift +// OCKSample +// +// Created by Corey Baker on 4/22/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import UIKit +import CareKitStore +import CareKitUI +import CareKit +import Contacts +import ContactsUI +import ParseSwift +import ParseCareKit +import os.log + +class MyContactViewController: OCKListViewController { + + fileprivate weak var contactDelegate: OCKContactViewControllerDelegate? + fileprivate var contacts = [OCKAnyContact]() + + /// The manager of the `Store` from which the `Contact` data is fetched. + public let storeManager: OCKSynchronizedStoreManager + + /// Initialize using a store manager. All of the contacts in the store manager will be queried and dispalyed. + /// + /// - Parameters: + /// - storeManager: The store manager owning the store whose contacts should be displayed. + public init(storeManager: OCKSynchronizedStoreManager) { + self.storeManager = storeManager + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + Task { + try? await fetchContacts() + } + } + + override func viewDidAppear(_ animated: Bool) { + Task { + try? await fetchContacts() + } + } + + override func appendViewController(_ viewController: UIViewController, animated: Bool) { + super.appendViewController(viewController, animated: animated) + + // Make sure this contact card matches app style when possible + if let carekitView = viewController.view as? OCKView { + carekitView.customStyle = CustomStyleKey.defaultValue + } + } + + @MainActor + func fetchContacts() async throws { + + guard User.current != nil, + let personUUIDString = ProfileViewModel.getRemoteClockUUIDAfterLoginFromLocalStorage()?.uuidString else { + Logger.myContact.error("User not logged in") + self.contacts.removeAll() + return + } + + /* + TODO: to get credit: How would you modify this query to only fetch the contact that belongs to this device? + + Hint 1: There are multiple ways to do this. You can modify the query + below which can work. + + Hint2: Look at the other queries in the app related to the uuid of the + user who's signed in. + + Hint3: You should have a warning currently, solving this properly would + get rid of the warning without changing the line the warning is on. + */ + var query = OCKContactQuery(for: Date()) + query.sortDescriptors.append(.familyName(ascending: true)) + query.sortDescriptors.append(.givenName(ascending: true)) + + self.contacts = try await storeManager.store.fetchAnyContacts(query: query) + self.displayContacts() + } + + @MainActor + func displayContacts() { + self.clear() + for contact in self.contacts { + let contactViewController = OCKDetailedContactViewController(contact: contact, + storeManager: storeManager) + contactViewController.delegate = self.contactDelegate + self.appendViewController(contactViewController, animated: false) + } + } +} + +extension MyContactViewController: OCKContactViewControllerDelegate { + + // swiftlint:disable:next line_length + func contactViewController(_ viewController: CareKit.OCKContactViewController, didEncounterError error: Error) where C: CareKit.OCKContactController, VS: CareKit.OCKContactViewSynchronizerProtocol { + + Logger.myContact.error("\(error.localizedDescription)") + } + +} diff --git a/OCKSample/MainTabs/Profile/MyContact/MyContactViewModel.swift b/OCKSample/MainTabs/Profile/MyContact/MyContactViewModel.swift new file mode 100644 index 0000000..28e6ece --- /dev/null +++ b/OCKSample/MainTabs/Profile/MyContact/MyContactViewModel.swift @@ -0,0 +1,32 @@ +// +// MyContactViewModel.swift +// OCKSample +// +// Created by Corey Baker on 4/21/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import CareKitStore +import os.log +import CareKit + +/* + You should notice this looks like ContactViewModel. +*/ + +class MyContactViewModel: ObservableObject { + @Published var update = false + + init() { + NotificationCenter.default.addObserver(self, selector: #selector(reloadViewModel), + name: Notification.Name(rawValue: Constants.storeInitialized), + object: nil) + } + + // MARK: Helpers + + @objc private func reloadViewModel() { + update = !update + } +} diff --git a/OCKSample/MainTabs/Profile/ProfileView.swift b/OCKSample/MainTabs/Profile/ProfileView.swift index 1202100..5f1d4f5 100644 --- a/OCKSample/MainTabs/Profile/ProfileView.swift +++ b/OCKSample/MainTabs/Profile/ProfileView.swift @@ -12,95 +12,224 @@ import CareKitStore import CareKit import os.log +// swiftlint:disable multiple_closures_with_trailing_closure + struct ProfileView: View { @Environment(\.presentationMode) var presentationMode - @Environment(\.userProfileViewModel) var viewModel + @Environment(\.tintColor) private var tintColor @EnvironmentObject var userStatus: UserStatus + @EnvironmentObject var profileViewModel: ProfileViewModel @State var firstName = "" @State var lastName = "" @State var birthday = Calendar.current.date(byAdding: .year, value: -20, to: Date())! + @State var note = "" + @State var sex = OCKBiologicalSex.other("unspecified") + @State private var sexOtherField = "" + @State private var street = "" + @State private var city = "" + @State private var state = "" + @State private var zipcode = "" + @State var showContact = false + @State var showingImagePicker = false var body: some View { + NavigationView { + VStack { + NavigationLink(isActive: $showContact, + destination: { + MyContactView() + .navigationBarTitle("My Contact Card") + }) { + EmptyView() + } + if let image = profileViewModel.profileUIImage { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100, alignment: .center) + .clipShape(Circle()) + .shadow(radius: 10) + .overlay(Circle().stroke(Color(tintColor), lineWidth: 5)) + .onTapGesture { + self.showingImagePicker = true + } + } else { + Image(systemName: "person.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100, alignment: .center) + .clipShape(Circle()) + .shadow(radius: 10) + .overlay(Circle().stroke(Color(tintColor), lineWidth: 5)) + .onTapGesture { + self.showingImagePicker = true + } + } - VStack { - VStack(alignment: .leading) { - TextField("First Name", text: $firstName) - .padding() - .cornerRadius(20.0) - .shadow(radius: 10.0, x: 20, y: 10) - - TextField("Last Name", text: $lastName) - .padding() - .cornerRadius(20.0) - .shadow(radius: 10.0, x: 20, y: 10) - - DatePicker("Birthday", selection: $birthday, displayedComponents: [DatePickerComponents.date]) - .padding() - .cornerRadius(20.0) - .shadow(radius: 10.0, x: 20, y: 10) - } - - // Notice that "action" is a closure (which is essentially - // a function as argument like we discussed in class) - Button(action: { - - Task { - do { - try await viewModel.saveProfile(firstName, - last: lastName, - birth: birthday) - } catch { - Logger.profile.error("Error saving profile: \(error.localizedDescription)") + VStack { + Form { + Section(header: Text("About")) { + TextField("First Name", text: $firstName) + TextField("Last Name", text: $lastName) + TextField("Note", text: $note) + DatePicker("Birthday", + selection: $birthday, + displayedComponents: [DatePickerComponents.date]) + + Picker(selection: $sex, label: Text("Sex"), content: { + Text(OCKBiologicalSex.female.rawValue).tag(OCKBiologicalSex.female) + Text(OCKBiologicalSex.male.rawValue).tag(OCKBiologicalSex.male) + TextField("Other", text: $sexOtherField).tag(OCKBiologicalSex.other(sexOtherField)) + }) + } + + Section(header: Text("Contact")) { + TextField("Street", text: $street) + TextField("City", text: $city) + TextField("State", text: $state) + TextField("Postal code", text: $zipcode) + } } - } - }, label: { - - Text("Save Profile") - .font(.headline) - .foregroundColor(.white) - .padding() - .frame(width: 300, height: 50) - }) - .background(Color(.green)) - .cornerRadius(15) - - // Notice that "action" is a closure (which is essentially - // a function as argument like we discussed in class) - Button(action: { - Task { - await viewModel.logout() + // Notice that "action" is a closure (which is essentially + // a function as argument like we discussed in class) + Button(action: { + + Task { + do { + try await profileViewModel.saveProfile(firstName, + last: lastName, + birth: birthday, + sex: sex, + note: note) + try await profileViewModel.saveContact(street, + city: city, + state: state, + zipcode: zipcode) + } catch { + Logger.profile.error("Error saving profile: \(error.localizedDescription)") + } + } + + }, label: { + + Text("Save Profile") + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(width: 300, height: 50) + }) + .background(Color(.green)) + .cornerRadius(15) + + // Notice that "action" is a closure (which is essentially + // a function as argument like we discussed in class) + Button(action: { + Task { + await profileViewModel.logout() + } + + }, label: { + + Text("Log Out") + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(width: 300, height: 50) + }) + .background(Color(.red)) + .cornerRadius(15) } - }, label: { - - Text("Log Out") - .font(.headline) - .foregroundColor(.white) - .padding() - .frame(width: 300, height: 50) - }) - .background(Color(.red)) - .cornerRadius(15) - }.onReceive(viewModel.$patient, perform: { patient in + Spacer() + } + .navigationBarItems(trailing: + Button(action: { + self.showContact = true + }) { + Text("My Contact") + }) + } + .sheet(isPresented: $showingImagePicker) { + ImagePicker(image: $profileViewModel.profileUIImage) + } + .onReceive(profileViewModel.$patient, perform: { patient in + // TODO: Be sure to update this list so changes are reflected in the view. if let currentFirstName = patient?.name.givenName { firstName = currentFirstName + } else { + /* + Else statements are default for when the view resets. For example, + when you logout and then login as a different user. + */ + firstName = "" } if let currentLastName = patient?.name.familyName { lastName = currentLastName + } else { + lastName = "" } if let currentBirthday = patient?.birthday { birthday = currentBirthday + } else { + birthday = Calendar.current.date(byAdding: .year, value: -20, to: Date())! + } + + if let currentNote = patient?.notes?.first?.content { + note = currentNote + } else { + note = "" + } + + if let currentSex = patient?.sex { + sex = currentSex + } else { + sex = OCKBiologicalSex.other("unspecified") + } + }) + .onReceive(profileViewModel.$contact, perform: { contact in + // TODO: Be sure to update this list so changes are reflected in the view. + if let currentStreet = contact?.address?.street { + street = currentStreet + } else { + street = "" + } + + if let currentCity = contact?.address?.city { + city = currentCity + } else { + city = "" } - }).onReceive(viewModel.$isLoggedOut, perform: { value in + + if let currentState = contact?.address?.state { + state = currentState + } else { + state = "" + } + + if let currentZipcode = contact?.address?.postalCode { + zipcode = currentZipcode + } else { + zipcode = "" + } + }) + .alert(isPresented: $profileViewModel.isShowingSaveAlert) { + return Alert(title: Text("Update"), + message: Text("All changs saved successfully!"), + dismissButton: .default(Text("Ok"), action: { + profileViewModel.isShowingSaveAlert = false + self.presentationMode.wrappedValue.dismiss() + })) + } + .onReceive(profileViewModel.$isLoggedOut, perform: { value in if self.userStatus.isLoggedOut != value { self.userStatus.check() } }).onAppear(perform: { - viewModel.refreshViewIfNeeded() + profileViewModel.refreshViewIfNeeded() }) } } @@ -109,5 +238,6 @@ struct ProfileView_Previews: PreviewProvider { static var previews: some View { ProfileView() .environmentObject(UserStatus(isLoggedOut: false)) + .environmentObject(ProfileViewModel()) } } diff --git a/OCKSample/MainTabs/Profile/ProfileViewModel.swift b/OCKSample/MainTabs/Profile/ProfileViewModel.swift index acdac15..f8a42cb 100644 --- a/OCKSample/MainTabs/Profile/ProfileViewModel.swift +++ b/OCKSample/MainTabs/Profile/ProfileViewModel.swift @@ -10,26 +10,61 @@ import Foundation import CareKit import CareKitStore import SwiftUI +import ParseSwift import ParseCareKit import UIKit import os.log import Combine +// swiftlint:disable type_body_length + class ProfileViewModel: ObservableObject { @Published var patient: OCKPatient? + @Published var contact: OCKContact? + @Published var sex: OCKBiologicalSex = .other("unspecified") + @Published var isShowingSaveAlert = false @Published var isLoggedOut = false { willSet { if newValue { error = nil patient = nil + contact = nil clearSubscriptions() } } } @Published public internal(set) var error: Error? + @Published var profileUIImage = UIImage(systemName: "person.fill") { + willSet { + guard self.profileUIImage != newValue, + let inputImage = newValue else { + return + } + + if !settingProfilePictureForFirstTime { + guard var user = User.current?.mergeable, + let image = inputImage.jpegData(compressionQuality: 0.25) else { + return + } + + let newProfilePicture = ParseFile(name: "profile.jpg", data: image) + user.profilePicture = newProfilePicture + let userToSave = user + Task { + do { + _ = try await userToSave.save() + Logger.profile.info("Saved updated profile picture successfully.") + } catch { + Logger.profile.error("Couldn't save profile picture: \(error.localizedDescription)") + } + } + } + } + } private(set) var storeManager: OCKSynchronizedStoreManager? private var cancellables: Set = [] + private var settingProfilePictureForFirstTime = true init() { reloadViewModel() @@ -62,6 +97,31 @@ class ProfileViewModel: ObservableObject { } } + @MainActor + private func fetchProfilePicture() async throws { + + // Profile pics are stored in Parse User. + guard let currentUser = try await User.current?.fetch() else { + Logger.profile.error("User isn't logged in") + return + } + + if let pictureFile = currentUser.profilePicture { + + // Download picture from server + do { + let profilePicture = try await pictureFile.fetch() + guard let path = profilePicture.localURL?.relativePath else { + return + } + self.profileUIImage = UIImage(contentsOfFile: path) + } catch { + Logger.profile.error("Couldn't fetch profile picture: \(error.localizedDescription).") + } + } + self.settingProfilePictureForFirstTime = false + } + @MainActor private func findAndObserveCurrentProfile() async { @@ -86,6 +146,20 @@ class ProfileViewModel: ObservableObject { return } self.observePatient(currentPatient) + + // Query the contact also so the user can edit + var queryForCurrentContact = OCKContactQuery(for: Date()) + queryForCurrentContact.ids = [uuid.uuidString] + + guard let foundContact = try await appDelegate.store?.fetchContacts(query: queryForCurrentContact), + let currentContact = foundContact.first else { + // swiftlint:disable:next line_length + Logger.profile.error("Error: Couldn't find contact with id \"\(uuid)\". It's possible they have never been saved.") + return + } + self.observeContact(currentContact) + + try? await fetchProfilePicture() } catch { // swiftlint:disable:next line_length Logger.profile.error("Error: Couldn't find patient with id \"\(uuid)\". It's possible they have never been saved. Query error: \(error.localizedDescription)") @@ -102,6 +176,16 @@ class ProfileViewModel: ObservableObject { .store(in: &cancellables) } + @MainActor + private func observeContact(_ contact: OCKContact) { + + storeManager?.publisher(forContact: contact, categories: [.add, .update, .delete]) + .sink { [weak self] in + self?.contact = $0 as? OCKContact + } + .store(in: &cancellables) + } + private func clearSubscriptions() { cancellables = [] } @@ -149,8 +233,24 @@ class ProfileViewModel: ObservableObject { } // MARK: User intentions + @MainActor - func saveProfile(_ first: String, last: String, birth: Date) async throws { + func saveProfile(_ first: String, + last: String, + birth: Date, + sex: OCKBiologicalSex, + note: String) async throws { + + /* + TODO: Be sure to this methods to save changes properly to OCKPatient. + */ + guard let remoteUUID = Self.getRemoteClockUUIDAfterLoginFromLocalStorage()?.uuidString else { + Logger.profile.error("Error: The user currently isn't logged in") + isLoggedOut = true + return + } + + isShowingSaveAlert = true // Make alert pop up for user. if var patientToUpdate = patient { // If there is a currentPatient that was fetched, check to see if any of the fields changed @@ -172,6 +272,17 @@ class ProfileViewModel: ObservableObject { patientToUpdate.birthday = birth } + if patient?.sex != sex { + patientHasBeenUpdated = true + patientToUpdate.sex = sex + } + + let notes = [OCKNote(author: first, title: "my note", content: note)] + if patient?.notes != notes { + patientHasBeenUpdated = true + patientToUpdate.notes = notes + } + if patientHasBeenUpdated { let updated = try await storeManager?.store.updateAnyPatient(patientToUpdate) Logger.profile.info("Successfully updated patient") @@ -182,12 +293,6 @@ class ProfileViewModel: ObservableObject { } } else { - // swiftlint:disable:next line_length - guard let remoteUUID = UserDefaults.standard.object(forKey: Constants.parseRemoteClockIDKey) as? String else { - Logger.profile.error("Error: The user currently isn't logged in") - isLoggedOut = true - return - } var newPatient = OCKPatient(id: remoteUUID, givenName: first, familyName: last) newPatient.birthday = birth @@ -202,6 +307,72 @@ class ProfileViewModel: ObservableObject { } } + @MainActor + func saveContact(_ street: String, + city: String, + state: String, + zipcode: String) async throws { + + /* + TODO: Be sure to this methods to save changes properly to OCKContact. + */ + + guard let remoteUUID = Self.getRemoteClockUUIDAfterLoginFromLocalStorage()?.uuidString else { + Logger.profile.error("Error: The user currently isn't logged in") + isLoggedOut = true + return + } + + if var contactToUpdate = contact { + // If there is a currentContact that was fetched, check to see if any of the fields changed + + var contactHasBeenUpdated = false + + // Since OCKPatient was updated earlier, we should compare against this name + if let patientName = patient?.name, + contact?.name != patient?.name { + contactHasBeenUpdated = true + contactToUpdate.name = patientName + } + + // Create a mutable temp address to compare + let potentialAddress = OCKPostalAddress() + potentialAddress.street = street + potentialAddress.city = city + potentialAddress.state = state + potentialAddress.postalCode = zipcode + + if contact?.address != potentialAddress { + contactHasBeenUpdated = true + contactToUpdate.address = potentialAddress + } + + if contactHasBeenUpdated { + let updated = try await storeManager?.store.updateAnyContact(contactToUpdate) + Logger.profile.info("Successfully updated contact") + guard let updatedContact = updated as? OCKContact else { + return + } + self.contact = updatedContact + } + + } else { + + guard let patientName = self.patient?.name else { + Logger.profile.info("The patient didn't have a name.") + return + } + + // Added code to create a contact for the respective signed up user + let newContact = OCKContact(id: remoteUUID, + name: patientName, + carePlanUUID: nil) + + // This is new contact that has never been saved before + _ = try await storeManager?.store.addAnyContact(newContact) + } + } + @MainActor static func savePatientAfterSignUp(_ type: UserType, first: String, last: String) async throws -> OCKPatient { @@ -232,8 +403,16 @@ class ProfileViewModel: ObservableObject { throw AppError.couldntCast } - try await appDelegate.store?.populateSampleData() - try await appDelegate.healthKitStore.populateSampleData() + // Added code to create a contact for the respective signed up user + let newContact = OCKContact(id: remoteUUID.uuidString, + name: newPatient.name, + carePlanUUID: nil) + + // This is new contact that has never been saved before + _ = try await storeManager.store.addAnyContact(newContact) + + try await appDelegate.store?.populateSampleData(patient.uuid) + try await appDelegate.healthKitStore.populateSampleData(patient.uuid) appDelegate.parseRemote.automaticallySynchronizes = true // Post notification to sync diff --git a/OCKSample/Models/User.swift b/OCKSample/Models/User.swift index 788ae57..1272f62 100644 --- a/OCKSample/Models/User.swift +++ b/OCKSample/Models/User.swift @@ -35,6 +35,8 @@ struct User: ParseUser { var userTypeUUIDs: [String: UUID]? + var profilePicture: ParseFile? + func merge(with object: Self) throws -> Self { var updated = try mergeParse(with: object) if updated.shouldRestoreKey(\.lastTypeSelected, @@ -45,6 +47,10 @@ struct User: ParseUser { original: object) { updated.userTypeUUIDs = object.userTypeUUIDs } + if updated.shouldRestoreKey(\.profilePicture, + original: object) { + updated.profilePicture = object.profilePicture + } return updated } }