diff --git a/CatchUp-SwiftUI.xcodeproj/project.pbxproj b/CatchUp-SwiftUI.xcodeproj/project.pbxproj index 1e837ee..d2df634 100644 --- a/CatchUp-SwiftUI.xcodeproj/project.pbxproj +++ b/CatchUp-SwiftUI.xcodeproj/project.pbxproj @@ -3,66 +3,79 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 144DBE22245C8282008FDBB6 /* PhoneNumberKit in Frameworks */ = {isa = PBXBuildFile; productRef = 144DBE21245C8282008FDBB6 /* PhoneNumberKit */; }; - 14ACAC86245E52A40091AE90 /* PhoneNumberMetadata.json in Resources */ = {isa = PBXBuildFile; fileRef = 14ACAC85245E52A40091AE90 /* PhoneNumberMetadata.json */; }; - 14BE3AD324593556004F72DE /* GeneralHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BE3AD224593556004F72DE /* GeneralHelpers.swift */; }; + 14BE3AD324593556004F72DE /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BE3AD224593556004F72DE /* Utils.swift */; }; 14BE3AD72459F610004F72DE /* UpdatesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BE3AD62459F610004F72DE /* UpdatesScreen.swift */; }; - F40293A1256186C2004E0418 /* SwiftUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = F40293A0256186C2004E0418 /* SwiftUIKit */; }; - F40293A725618C4C004E0418 /* ContactHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40293A625618C4C004E0418 /* ContactHelper.swift */; }; F4095B6424C66F87007163E3 /* SKProduct+localizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4095B6324C66F87007163E3 /* SKProduct+localizedPrice.swift */; }; - F416505A24440579001DB205 /* CatchUp-SwiftUI.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F416505824440579001DB205 /* CatchUp-SwiftUI.xcdatamodeld */; }; - F4871DA1244FC43C00925392 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4871DA0244FC43C00925392 /* NotificationService.swift */; }; - F48E37F222C455C3008B0B8B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E37F122C455C3008B0B8B /* AppDelegate.swift */; }; - F48E37F422C455C3008B0B8B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E37F322C455C3008B0B8B /* SceneDelegate.swift */; }; + F4118B452B9E68AE001BC8C7 /* ContactPickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4118B442B9E68AE001BC8C7 /* ContactPickerDelegate.swift */; }; + F4118B492B9E8FC7001BC8C7 /* ContactRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4118B482B9E8FC7001BC8C7 /* ContactRowView.swift */; }; + F4118B4B2B9E912E001BC8C7 /* OpenContactPickerButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4118B4A2B9E912E001BC8C7 /* OpenContactPickerButtonView.swift */; }; + F4118B4D2B9EA11A001BC8C7 /* NameAndPreferenceStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4118B4C2B9EA11A001BC8C7 /* NameAndPreferenceStack.swift */; }; + F4118B4F2B9EA168001BC8C7 /* ContactInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4118B4E2B9EA168001BC8C7 /* ContactInfoView.swift */; }; + F435D42F2BB78A6F00C43586 /* NotificationPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F435D42E2BB78A6F00C43586 /* NotificationPreferenceView.swift */; }; + F435D4312BB7E1D300C43586 /* NextCatchUpRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F435D4302BB7E1D300C43586 /* NextCatchUpRow.swift */; }; + F435D4332BB8E7E700C43586 /* RemoveContactButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F435D4322BB8E7E700C43586 /* RemoveContactButton.swift */; }; + F435D4352BBA3EB100C43586 /* BirthdayOrAnniversaryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F435D4342BBA3EB100C43586 /* BirthdayOrAnniversaryRow.swift */; }; + F435D4372BBA4EC600C43586 /* DataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F435D4362BBA4EC600C43586 /* DataController.swift */; }; + F435D4392BBB7B7800C43586 /* NoContactSelectedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F435D4382BBB7B7800C43586 /* NoContactSelectedScreen.swift */; }; + F4871DA1244FC43C00925392 /* NotificationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4871DA0244FC43C00925392 /* NotificationHelper.swift */; }; F48E37F922C455C3008B0B8B /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E37F822C455C3008B0B8B /* HomeScreen.swift */; }; F48E37FB22C455CB008B0B8B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F48E37FA22C455CB008B0B8B /* Assets.xcassets */; }; F48E37FE22C455CB008B0B8B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F48E37FD22C455CB008B0B8B /* Preview Assets.xcassets */; }; - F48E380122C455CB008B0B8B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F48E37FF22C455CB008B0B8B /* LaunchScreen.storyboard */; }; - F494F95824424A03003CE7B5 /* ContactService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F494F95724424A03003CE7B5 /* ContactService.swift */; }; + F494F95824424A03003CE7B5 /* ContactHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F494F95724424A03003CE7B5 /* ContactHelper.swift */; }; F4A9B8522442A0F5001D8C55 /* DetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A9B8512442A0F5001D8C55 /* DetailScreen.swift */; }; F4A9B8552442A179001D8C55 /* ContactPhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A9B8542442A179001D8C55 /* ContactPhoto.swift */; }; F4A9B8572442A1F7001D8C55 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A9B8562442A1F7001D8C55 /* GradientView.swift */; }; - F4A9B8592442AB81001D8C55 /* PreferenceScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A9B8582442AB81001D8C55 /* PreferenceScreen.swift */; }; F4A9B85B2443FFF3001D8C55 /* Conversions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A9B85A2443FFF3001D8C55 /* Conversions.swift */; }; - F4AD59AA244A960800296568 /* SelectedContact+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4AD59A8244A960800296568 /* SelectedContact+CoreDataClass.swift */; }; - F4AD59AB244A960800296568 /* SelectedContact+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4AD59A9244A960800296568 /* SelectedContact+CoreDataProperties.swift */; }; F4AD59AE244C9FF600296568 /* IAPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4AD59AD244C9FF600296568 /* IAPService.swift */; }; F4AD59B0244CA12C00296568 /* AboutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4AD59AF244CA12C00296568 /* AboutScreen.swift */; }; - F4AD59B3244CC9D600296568 /* UserDefaults+isFirstLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4AD59B2244CC9D600296568 /* UserDefaults+isFirstLaunch.swift */; }; + F4BAD42D2B94E45D0009CD50 /* SelectedContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BAD42C2B94E45D0009CD50 /* SelectedContact.swift */; }; + F4BAD42F2B94E8740009CD50 /* CatchUpApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BAD42E2B94E8740009CD50 /* CatchUpApp.swift */; }; + F4BAD4312B94F5680009CD50 /* ModelContext+sqliteCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BAD4302B94F5680009CD50 /* ModelContext+sqliteCommand.swift */; }; + F4F7535D2BB0956800B20090 /* NextCatchUpsGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F7535C2BB0956800B20090 /* NextCatchUpsGridView.swift */; }; + F4F7535F2BB0969A00B20090 /* ContactPictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F7535E2BB0969A00B20090 /* ContactPictureView.swift */; }; + F4F753652BB11FA300B20090 /* View+if.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F753642BB11FA300B20090 /* View+if.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 14ACAC85245E52A40091AE90 /* PhoneNumberMetadata.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = PhoneNumberMetadata.json; sourceTree = ""; }; - 14BE3AD224593556004F72DE /* GeneralHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralHelpers.swift; sourceTree = ""; }; + 14BE3AD224593556004F72DE /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 14BE3AD62459F610004F72DE /* UpdatesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatesScreen.swift; sourceTree = ""; }; - F40293A625618C4C004E0418 /* ContactHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactHelper.swift; sourceTree = ""; }; F4095B6324C66F87007163E3 /* SKProduct+localizedPrice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+localizedPrice.swift"; sourceTree = ""; }; - F416505924440579001DB205 /* CatchUp_SwiftUI.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CatchUp_SwiftUI.xcdatamodel; sourceTree = ""; }; - F4871DA0244FC43C00925392 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + F4118B442B9E68AE001BC8C7 /* ContactPickerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPickerDelegate.swift; sourceTree = ""; }; + F4118B482B9E8FC7001BC8C7 /* ContactRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRowView.swift; sourceTree = ""; }; + F4118B4A2B9E912E001BC8C7 /* OpenContactPickerButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenContactPickerButtonView.swift; sourceTree = ""; }; + F4118B4C2B9EA11A001BC8C7 /* NameAndPreferenceStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameAndPreferenceStack.swift; sourceTree = ""; }; + F4118B4E2B9EA168001BC8C7 /* ContactInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoView.swift; sourceTree = ""; }; + F435D42E2BB78A6F00C43586 /* NotificationPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPreferenceView.swift; sourceTree = ""; }; + F435D4302BB7E1D300C43586 /* NextCatchUpRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextCatchUpRow.swift; sourceTree = ""; }; + F435D4322BB8E7E700C43586 /* RemoveContactButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveContactButton.swift; sourceTree = ""; }; + F435D4342BBA3EB100C43586 /* BirthdayOrAnniversaryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BirthdayOrAnniversaryRow.swift; sourceTree = ""; }; + F435D4362BBA4EC600C43586 /* DataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataController.swift; sourceTree = ""; }; + F435D4382BBB7B7800C43586 /* NoContactSelectedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoContactSelectedScreen.swift; sourceTree = ""; }; + F4871DA0244FC43C00925392 /* NotificationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHelper.swift; sourceTree = ""; }; F48E37EE22C455C3008B0B8B /* CatchUp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CatchUp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - F48E37F122C455C3008B0B8B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - F48E37F322C455C3008B0B8B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; F48E37F822C455C3008B0B8B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; F48E37FA22C455CB008B0B8B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F48E37FD22C455CB008B0B8B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - F48E380022C455CB008B0B8B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F48E380222C455CB008B0B8B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F494F95724424A03003CE7B5 /* ContactService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactService.swift; sourceTree = ""; }; + F494F95724424A03003CE7B5 /* ContactHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactHelper.swift; sourceTree = ""; }; F4A9B8512442A0F5001D8C55 /* DetailScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailScreen.swift; sourceTree = ""; }; F4A9B8542442A179001D8C55 /* ContactPhoto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPhoto.swift; sourceTree = ""; }; F4A9B8562442A1F7001D8C55 /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; - F4A9B8582442AB81001D8C55 /* PreferenceScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceScreen.swift; sourceTree = ""; }; F4A9B85A2443FFF3001D8C55 /* Conversions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conversions.swift; sourceTree = ""; }; - F4AD59A8244A960800296568 /* SelectedContact+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SelectedContact+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; }; - F4AD59A9244A960800296568 /* SelectedContact+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SelectedContact+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; }; F4AD59AC244B7B6000296568 /* CatchUp-SwiftUI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "CatchUp-SwiftUI.entitlements"; sourceTree = ""; }; F4AD59AD244C9FF600296568 /* IAPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPService.swift; sourceTree = ""; }; F4AD59AF244CA12C00296568 /* AboutScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutScreen.swift; sourceTree = ""; }; - F4AD59B2244CC9D600296568 /* UserDefaults+isFirstLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+isFirstLaunch.swift"; sourceTree = ""; }; + F4BAD42C2B94E45D0009CD50 /* SelectedContact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedContact.swift; sourceTree = ""; }; + F4BAD42E2B94E8740009CD50 /* CatchUpApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatchUpApp.swift; sourceTree = ""; }; + F4BAD4302B94F5680009CD50 /* ModelContext+sqliteCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ModelContext+sqliteCommand.swift"; sourceTree = ""; }; + F4F7535C2BB0956800B20090 /* NextCatchUpsGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextCatchUpsGridView.swift; sourceTree = ""; }; + F4F7535E2BB0969A00B20090 /* ContactPictureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPictureView.swift; sourceTree = ""; }; + F4F753642BB11FA300B20090 /* View+if.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+if.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -71,37 +84,59 @@ buildActionMask = 2147483647; files = ( 144DBE22245C8282008FDBB6 /* PhoneNumberKit in Frameworks */, - F40293A1256186C2004E0418 /* SwiftUIKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 144DBE23245C864F008FDBB6 /* Resources */ = { + F4118B472B9E8E10001BC8C7 /* Views */ = { isa = PBXGroup; children = ( - 14ACAC85245E52A40091AE90 /* PhoneNumberMetadata.json */, + F48E37F822C455C3008B0B8B /* HomeScreen.swift */, + F4118B502B9EA2E3001BC8C7 /* HomeScreen Subviews */, + F4A9B8512442A0F5001D8C55 /* DetailScreen.swift */, + F4118B512B9EA2F7001BC8C7 /* DetailScreen Subviews */, + F4AD59AF244CA12C00296568 /* AboutScreen.swift */, + 14BE3AD62459F610004F72DE /* UpdatesScreen.swift */, + F435D4382BBB7B7800C43586 /* NoContactSelectedScreen.swift */, ); - path = Resources; + path = Views; sourceTree = ""; }; - F42440B324468480001ABD14 /* CoreData */ = { + F4118B502B9EA2E3001BC8C7 /* HomeScreen Subviews */ = { isa = PBXGroup; children = ( - F4AD59A8244A960800296568 /* SelectedContact+CoreDataClass.swift */, - F4AD59A9244A960800296568 /* SelectedContact+CoreDataProperties.swift */, - F416505824440579001DB205 /* CatchUp-SwiftUI.xcdatamodeld */, + F4118B482B9E8FC7001BC8C7 /* ContactRowView.swift */, + F4F7535E2BB0969A00B20090 /* ContactPictureView.swift */, + F4118B4A2B9E912E001BC8C7 /* OpenContactPickerButtonView.swift */, + F4F7535C2BB0956800B20090 /* NextCatchUpsGridView.swift */, + ); + path = "HomeScreen Subviews"; + sourceTree = ""; + }; + F4118B512B9EA2F7001BC8C7 /* DetailScreen Subviews */ = { + isa = PBXGroup; + children = ( + F4A9B8562442A1F7001D8C55 /* GradientView.swift */, + F4A9B8542442A179001D8C55 /* ContactPhoto.swift */, + F4118B4C2B9EA11A001BC8C7 /* NameAndPreferenceStack.swift */, + F4118B4E2B9EA168001BC8C7 /* ContactInfoView.swift */, + F435D42E2BB78A6F00C43586 /* NotificationPreferenceView.swift */, + F435D4302BB7E1D300C43586 /* NextCatchUpRow.swift */, + F435D4322BB8E7E700C43586 /* RemoveContactButton.swift */, + F435D4342BBA3EB100C43586 /* BirthdayOrAnniversaryRow.swift */, ); - path = CoreData; + path = "DetailScreen Subviews"; sourceTree = ""; }; F4871DA2244FC44800925392 /* Utilities */ = { isa = PBXGroup; children = ( F4A9B85A2443FFF3001D8C55 /* Conversions.swift */, - 14BE3AD224593556004F72DE /* GeneralHelpers.swift */, - F40293A625618C4C004E0418 /* ContactHelper.swift */, + 14BE3AD224593556004F72DE /* Utils.swift */, + F494F95724424A03003CE7B5 /* ContactHelper.swift */, + F4871DA0244FC43C00925392 /* NotificationHelper.swift */, ); path = Utilities; sourceTree = ""; @@ -125,24 +160,16 @@ F48E37F022C455C3008B0B8B /* CatchUp-SwiftUI */ = { isa = PBXGroup; children = ( + F4BAD42E2B94E8740009CD50 /* CatchUpApp.swift */, F4AD59AC244B7B6000296568 /* CatchUp-SwiftUI.entitlements */, F48E37FA22C455CB008B0B8B /* Assets.xcassets */, - F48E37F122C455C3008B0B8B /* AppDelegate.swift */, - F48E37F322C455C3008B0B8B /* SceneDelegate.swift */, - F48E37FF22C455CB008B0B8B /* LaunchScreen.storyboard */, F48E380222C455CB008B0B8B /* Info.plist */, - 144DBE23245C864F008FDBB6 /* Resources */, - F4AD59B1244CC9C100296568 /* Extensions */, + F4118B472B9E8E10001BC8C7 /* Views */, + F4BAD4292B94E41F0009CD50 /* Data */, F4A9B85024429E40001D8C55 /* Services */, F4871DA2244FC44800925392 /* Utilities */, - F4A9B8532442A162001D8C55 /* Supporting Views */, - F42440B324468480001ABD14 /* CoreData */, + F4AD59B1244CC9C100296568 /* Extensions */, F48E37FC22C455CB008B0B8B /* Preview Content */, - F48E37F822C455C3008B0B8B /* HomeScreen.swift */, - F4A9B8512442A0F5001D8C55 /* DetailScreen.swift */, - F4A9B8582442AB81001D8C55 /* PreferenceScreen.swift */, - F4AD59AF244CA12C00296568 /* AboutScreen.swift */, - 14BE3AD62459F610004F72DE /* UpdatesScreen.swift */, ); path = "CatchUp-SwiftUI"; sourceTree = ""; @@ -158,29 +185,29 @@ F4A9B85024429E40001D8C55 /* Services */ = { isa = PBXGroup; children = ( - F494F95724424A03003CE7B5 /* ContactService.swift */, F4AD59AD244C9FF600296568 /* IAPService.swift */, - F4871DA0244FC43C00925392 /* NotificationService.swift */, + F4118B442B9E68AE001BC8C7 /* ContactPickerDelegate.swift */, ); path = Services; sourceTree = ""; }; - F4A9B8532442A162001D8C55 /* Supporting Views */ = { + F4AD59B1244CC9C100296568 /* Extensions */ = { isa = PBXGroup; children = ( - F4A9B8542442A179001D8C55 /* ContactPhoto.swift */, - F4A9B8562442A1F7001D8C55 /* GradientView.swift */, + F4095B6324C66F87007163E3 /* SKProduct+localizedPrice.swift */, + F4BAD4302B94F5680009CD50 /* ModelContext+sqliteCommand.swift */, + F4F753642BB11FA300B20090 /* View+if.swift */, ); - path = "Supporting Views"; + path = Extensions; sourceTree = ""; }; - F4AD59B1244CC9C100296568 /* Extensions */ = { + F4BAD4292B94E41F0009CD50 /* Data */ = { isa = PBXGroup; children = ( - F4AD59B2244CC9D600296568 /* UserDefaults+isFirstLaunch.swift */, - F4095B6324C66F87007163E3 /* SKProduct+localizedPrice.swift */, + F435D4362BBA4EC600C43586 /* DataController.swift */, + F4BAD42C2B94E45D0009CD50 /* SelectedContact.swift */, ); - path = Extensions; + path = Data; sourceTree = ""; }; /* End PBXGroup section */ @@ -201,7 +228,6 @@ name = "CatchUp-SwiftUI"; packageProductDependencies = ( 144DBE21245C8282008FDBB6 /* PhoneNumberKit */, - F40293A0256186C2004E0418 /* SwiftUIKit */, ); productName = "CatchUp-SwiftUI"; productReference = F48E37EE22C455C3008B0B8B /* CatchUp.app */; @@ -213,8 +239,9 @@ F48E37E622C455C3008B0B8B /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1100; - LastUpgradeCheck = 1200; + LastUpgradeCheck = 1530; ORGANIZATIONNAME = "Token Solutions"; TargetAttributes = { F48E37ED22C455C3008B0B8B = { @@ -233,7 +260,6 @@ mainGroup = F48E37E522C455C3008B0B8B; packageReferences = ( 144DBE20245C8282008FDBB6 /* XCRemoteSwiftPackageReference "PhoneNumberKit" */, - F402939F256186C2004E0418 /* XCRemoteSwiftPackageReference "SwiftUIKit" */, ); productRefGroup = F48E37EF22C455C3008B0B8B /* Products */; projectDirPath = ""; @@ -249,8 +275,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 14ACAC86245E52A40091AE90 /* PhoneNumberMetadata.json in Resources */, - F48E380122C455CB008B0B8B /* LaunchScreen.storyboard in Resources */, F48E37FE22C455CB008B0B8B /* Preview Assets.xcassets in Resources */, F48E37FB22C455CB008B0B8B /* Assets.xcassets in Resources */, ); @@ -263,47 +287,46 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F48E37F222C455C3008B0B8B /* AppDelegate.swift in Sources */, F4A9B8552442A179001D8C55 /* ContactPhoto.swift in Sources */, + F4118B4B2B9E912E001BC8C7 /* OpenContactPickerButtonView.swift in Sources */, F4A9B8572442A1F7001D8C55 /* GradientView.swift in Sources */, F4A9B85B2443FFF3001D8C55 /* Conversions.swift in Sources */, - 14BE3AD324593556004F72DE /* GeneralHelpers.swift in Sources */, - F4871DA1244FC43C00925392 /* NotificationService.swift in Sources */, - F494F95824424A03003CE7B5 /* ContactService.swift in Sources */, + 14BE3AD324593556004F72DE /* Utils.swift in Sources */, + F435D4392BBB7B7800C43586 /* NoContactSelectedScreen.swift in Sources */, + F4F753652BB11FA300B20090 /* View+if.swift in Sources */, + F4871DA1244FC43C00925392 /* NotificationHelper.swift in Sources */, + F4F7535D2BB0956800B20090 /* NextCatchUpsGridView.swift in Sources */, + F4118B4D2B9EA11A001BC8C7 /* NameAndPreferenceStack.swift in Sources */, + F494F95824424A03003CE7B5 /* ContactHelper.swift in Sources */, + F4118B4F2B9EA168001BC8C7 /* ContactInfoView.swift in Sources */, + F435D42F2BB78A6F00C43586 /* NotificationPreferenceView.swift in Sources */, F4095B6424C66F87007163E3 /* SKProduct+localizedPrice.swift in Sources */, - F416505A24440579001DB205 /* CatchUp-SwiftUI.xcdatamodeld in Sources */, + F4BAD42F2B94E8740009CD50 /* CatchUpApp.swift in Sources */, 14BE3AD72459F610004F72DE /* UpdatesScreen.swift in Sources */, F4AD59B0244CA12C00296568 /* AboutScreen.swift in Sources */, - F4A9B8592442AB81001D8C55 /* PreferenceScreen.swift in Sources */, - F4AD59AA244A960800296568 /* SelectedContact+CoreDataClass.swift in Sources */, - F4AD59B3244CC9D600296568 /* UserDefaults+isFirstLaunch.swift in Sources */, - F4AD59AB244A960800296568 /* SelectedContact+CoreDataProperties.swift in Sources */, + F4BAD42D2B94E45D0009CD50 /* SelectedContact.swift in Sources */, F48E37F922C455C3008B0B8B /* HomeScreen.swift in Sources */, + F435D4312BB7E1D300C43586 /* NextCatchUpRow.swift in Sources */, + F435D4332BB8E7E700C43586 /* RemoveContactButton.swift in Sources */, + F435D4352BBA3EB100C43586 /* BirthdayOrAnniversaryRow.swift in Sources */, + F435D4372BBA4EC600C43586 /* DataController.swift in Sources */, F4AD59AE244C9FF600296568 /* IAPService.swift in Sources */, - F48E37F422C455C3008B0B8B /* SceneDelegate.swift in Sources */, + F4F7535F2BB0969A00B20090 /* ContactPictureView.swift in Sources */, + F4BAD4312B94F5680009CD50 /* ModelContext+sqliteCommand.swift in Sources */, F4A9B8522442A0F5001D8C55 /* DetailScreen.swift in Sources */, - F40293A725618C4C004E0418 /* ContactHelper.swift in Sources */, + F4118B492B9E8FC7001BC8C7 /* ContactRowView.swift in Sources */, + F4118B452B9E68AE001BC8C7 /* ContactPickerDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXVariantGroup section */ - F48E37FF22C455CB008B0B8B /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F48E380022C455CB008B0B8B /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ F48E380322C455CB008B0B8B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -337,6 +360,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -351,7 +375,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -367,6 +391,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -400,6 +425,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -408,7 +434,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -431,15 +457,18 @@ DEVELOPMENT_TEAM = DJAHJZFYK7; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = "CatchUp-SwiftUI/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + INFOPLIST_KEY_CFBundleDisplayName = CatchUp; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.3; + MARKETING_VERSION = 3.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.tokensolutions.CatchUp; PRODUCT_NAME = CatchUp; SUPPORTS_MACCATALYST = NO; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -456,15 +485,18 @@ DEVELOPMENT_TEAM = DJAHJZFYK7; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = "CatchUp-SwiftUI/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + INFOPLIST_KEY_CFBundleDisplayName = CatchUp; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.3; + MARKETING_VERSION = 3.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.tokensolutions.CatchUp; PRODUCT_NAME = CatchUp; SUPPORTS_MACCATALYST = NO; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -502,14 +534,6 @@ minimumVersion = 3.2.0; }; }; - F402939F256186C2004E0418 /* XCRemoteSwiftPackageReference "SwiftUIKit" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/youjinp/SwiftUIKit.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.0.11; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -518,25 +542,7 @@ package = 144DBE20245C8282008FDBB6 /* XCRemoteSwiftPackageReference "PhoneNumberKit" */; productName = PhoneNumberKit; }; - F40293A0256186C2004E0418 /* SwiftUIKit */ = { - isa = XCSwiftPackageProductDependency; - package = F402939F256186C2004E0418 /* XCRemoteSwiftPackageReference "SwiftUIKit" */; - productName = SwiftUIKit; - }; /* End XCSwiftPackageProductDependency section */ - -/* Begin XCVersionGroup section */ - F416505824440579001DB205 /* CatchUp-SwiftUI.xcdatamodeld */ = { - isa = XCVersionGroup; - children = ( - F416505924440579001DB205 /* CatchUp_SwiftUI.xcdatamodel */, - ); - currentVersion = F416505924440579001DB205 /* CatchUp_SwiftUI.xcdatamodel */; - path = "CatchUp-SwiftUI.xcdatamodeld"; - sourceTree = ""; - versionGroupType = wrapper.xcdatamodel; - }; -/* End XCVersionGroup section */ }; rootObject = F48E37E622C455C3008B0B8B /* Project object */; } diff --git a/CatchUp-SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CatchUp-SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 83fe7ec..bebd010 100644 --- a/CatchUp-SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CatchUp-SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,25 +1,15 @@ { - "object": { - "pins": [ - { - "package": "PhoneNumberKit", - "repositoryURL": "https://github.com/marmelroy/PhoneNumberKit.git", - "state": { - "branch": null, - "revision": "5c8c906036dd44d0ac6d721c5fd0bf5602d1ff0e", - "version": "3.2.0" - } - }, - { - "package": "SwiftUIKit", - "repositoryURL": "https://github.com/youjinp/SwiftUIKit.git", - "state": { - "branch": null, - "revision": "16dcc576034f92e3e3429e820e52f414f7f800cd", - "version": "0.0.11" - } + "originHash" : "d01b822f6446cef501cebe37b5b78101e0a82173bb523ee64501d3331d40b50c", + "pins" : [ + { + "identity" : "phonenumberkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/marmelroy/PhoneNumberKit.git", + "state" : { + "revision" : "ee5d7114934e60812c9b47c333f01b67d002be2d", + "version" : "3.7.10" } - ] - }, - "version": 1 + } + ], + "version" : 3 } diff --git a/CatchUp-SwiftUI.xcodeproj/xcshareddata/xcschemes/CatchUp-SwiftUI.xcscheme b/CatchUp-SwiftUI.xcodeproj/xcshareddata/xcschemes/CatchUp-SwiftUI.xcscheme new file mode 100644 index 0000000..bc0b26a --- /dev/null +++ b/CatchUp-SwiftUI.xcodeproj/xcshareddata/xcschemes/CatchUp-SwiftUI.xcscheme @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CatchUp-SwiftUI/AppDelegate.swift b/CatchUp-SwiftUI/AppDelegate.swift deleted file mode 100644 index 7b222ae..0000000 --- a/CatchUp-SwiftUI/AppDelegate.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// AppDelegate.swift -// CatchUp-SwiftUI -// -// Created by Ryan Token on 6/26/19. -// Copyright © 2019 Token Solutions. All rights reserved. -// - -import UIKit -import CoreData -import UserNotifications - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - if UserDefaults.isFirstVersion2Launch() { - UNUserNotificationCenter.current().removeAllPendingNotificationRequests() - } - - return true - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - // Saves changes in the application's managed object context before the application terminates. - self.saveContext() - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - - // MARK: - Core Data stack - - lazy var persistentContainer: NSPersistentCloudKitContainer = { - /* - The persistent container for the application. This implementation - creates and returns a container, having loaded the store for the - application to it. This property is optional since there are legitimate - error conditions that could cause the creation of the store to fail. - */ - let container = NSPersistentCloudKitContainer(name: "CatchUp-SwiftUI") - 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)") - } - }) - return container - }() - - // MARK: - Core Data Saving support - - func saveContext () { - let context = persistentContainer.viewContext - if context.hasChanges { - do { - try context.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nserror = error as NSError - fatalError("Unresolved error \(nserror), \(nserror.userInfo)") - } - } - } - -} - diff --git a/CatchUp-SwiftUI/Assets.xcassets/Contents.json b/CatchUp-SwiftUI/Assets.xcassets/Contents.json index da4a164..73c0059 100644 --- a/CatchUp-SwiftUI/Assets.xcassets/Contents.json +++ b/CatchUp-SwiftUI/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/CatchUp-SwiftUI/Assets.xcassets/LaunchScreenBackgroundColor.colorset/Contents.json b/CatchUp-SwiftUI/Assets.xcassets/LaunchScreenBackgroundColor.colorset/Contents.json new file mode 100644 index 0000000..be9d677 --- /dev/null +++ b/CatchUp-SwiftUI/Assets.xcassets/LaunchScreenBackgroundColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CatchUp-SwiftUI/Base.lproj/LaunchScreen.storyboard b/CatchUp-SwiftUI/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 3fa9507..0000000 --- a/CatchUp-SwiftUI/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CatchUp-SwiftUI/CatchUp-SwiftUI.entitlements b/CatchUp-SwiftUI/CatchUp-SwiftUI.entitlements index db528e0..ee7040e 100644 --- a/CatchUp-SwiftUI/CatchUp-SwiftUI.entitlements +++ b/CatchUp-SwiftUI/CatchUp-SwiftUI.entitlements @@ -2,6 +2,16 @@ + aps-environment + development + com.apple.developer.icloud-container-identifiers + + iCloud.com.ryantoken.CatchUp + + com.apple.developer.icloud-services + + CloudKit + com.apple.security.app-sandbox com.apple.security.network.client diff --git a/CatchUp-SwiftUI/CatchUpApp.swift b/CatchUp-SwiftUI/CatchUpApp.swift new file mode 100644 index 0000000..616111e --- /dev/null +++ b/CatchUp-SwiftUI/CatchUpApp.swift @@ -0,0 +1,46 @@ +// +// CatchUpApp.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/3/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import SwiftData +import SwiftUI + +@main +struct CatchUpApp: App { + // use the SQLite file created by Core Data originally, instead of SwiftData's default.store file + let url = URL.applicationSupportDirectory.appending(path: "CatchUp-SwiftUI.sqlite") + let modelContainer: ModelContainer + + @State private var dataController = DataController() + + init() { + do { + modelContainer = try ModelContainer( + for: SelectedContact.self, + configurations: ModelConfiguration(url: url)) + } catch { + fatalError("Failed to initialize model container.") + } + } + + var body: some Scene { + WindowGroup { + NavigationSplitView { + HomeScreen() + } detail: { + if let selectedContact = dataController.selectedContact { + DetailScreen(contact: selectedContact) + } else { + NoContactSelectedScreen() + } + } + .environment(dataController) + .accentColor(.orange) + } + .modelContainer(modelContainer) + } +} diff --git a/CatchUp-SwiftUI/CatchUp_SwiftUI.entitlements b/CatchUp-SwiftUI/CatchUp_SwiftUI.entitlements new file mode 100644 index 0000000..f2ef3ae --- /dev/null +++ b/CatchUp-SwiftUI/CatchUp_SwiftUI.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/CatchUp-SwiftUI/CoreData/CatchUp-SwiftUI.xcdatamodeld/CatchUp_SwiftUI.xcdatamodel/contents b/CatchUp-SwiftUI/CoreData/CatchUp-SwiftUI.xcdatamodeld/CatchUp_SwiftUI.xcdatamodel/contents index e921421..61c3737 100644 --- a/CatchUp-SwiftUI/CoreData/CatchUp-SwiftUI.xcdatamodeld/CatchUp_SwiftUI.xcdatamodel/contents +++ b/CatchUp-SwiftUI/CoreData/CatchUp-SwiftUI.xcdatamodeld/CatchUp_SwiftUI.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -23,7 +23,4 @@ - - - \ No newline at end of file diff --git a/CatchUp-SwiftUI/Data/DataController.swift b/CatchUp-SwiftUI/Data/DataController.swift new file mode 100644 index 0000000..efb02d0 --- /dev/null +++ b/CatchUp-SwiftUI/Data/DataController.swift @@ -0,0 +1,14 @@ +// +// DataController.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/31/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import Foundation + +@Observable +class DataController { + var selectedContact: SelectedContact? +} diff --git a/CatchUp-SwiftUI/Data/SelectedContact.swift b/CatchUp-SwiftUI/Data/SelectedContact.swift new file mode 100644 index 0000000..a940234 --- /dev/null +++ b/CatchUp-SwiftUI/Data/SelectedContact.swift @@ -0,0 +1,118 @@ +// +// SelectedContact.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/3/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// +// + +import Foundation +import SwiftData + +@Model +class SelectedContact { + var address: String = "" + var anniversary: String = "" + var anniversary_notification_id: UUID = UUID() + var birthday: String = "" + var birthday_notification_id: UUID = UUID() + var email: String = "" + var id: UUID = UUID() + var name: String = "" + var notification_identifier: UUID = UUID() + var notification_preference: Int = 0 + var notification_preference_custom_day: Int = 0 + var notification_preference_custom_month: Int = 0 + var notification_preference_custom_year: Int = 0 + var notification_preference_hour: Int = 0 + var notification_preference_minute: Int = 0 + var notification_preference_weekday: Int = 0 + var notification_preference_week_of_month: Int = 0 + var phone: String = "" + var picture: String = "" + var secondary_address: String = "" + var secondary_email: String = "" + var secondary_phone: String = "" + var next_notification_date_time: String = "" + var unread_badge_date_time: String = "" + + init( + address: String, + anniversary: String, + anniversary_notification_id: UUID, + birthday: String, + birthday_notification_id: UUID, + email: String, + id: UUID, + name: String, + next_notification_date_time: String, + notification_identifier: UUID, + notification_preference: Int, + notification_preference_custom_day: Int, + notification_preference_custom_month: Int, + notification_preference_custom_year: Int, + notification_preference_hour: Int, + notification_preference_minute: Int, + notification_preference_weekday: Int, + notification_preference_week_of_month: Int, + phone: String, + picture: String, + secondary_address: String, + secondary_email: String, + secondary_phone: String, + unread_badge_date_time: String + ) { + self.address = address + self.anniversary = anniversary + self.anniversary_notification_id = anniversary_notification_id + self.birthday = birthday + self.birthday_notification_id = birthday_notification_id + self.email = email + self.id = id + self.name = name + self.next_notification_date_time = next_notification_date_time + self.notification_identifier = notification_identifier + self.notification_preference = notification_preference + self.notification_preference_custom_day = notification_preference_custom_day + self.notification_preference_custom_month = notification_preference_custom_month + self.notification_preference_custom_year = notification_preference_custom_year + self.notification_preference_hour = notification_preference_hour + self.notification_preference_minute = notification_preference_minute + self.notification_preference_weekday = notification_preference_weekday + self.notification_preference_week_of_month = notification_preference_week_of_month + self.phone = phone + self.picture = picture + self.secondary_address = secondary_address + self.secondary_email = secondary_email + self.secondary_phone = secondary_phone + self.unread_badge_date_time = unread_badge_date_time + } + + static let sampleData = SelectedContact( + address: "2190 E 11th Ave", + anniversary: "06/20/2020", + anniversary_notification_id: UUID(), + birthday: "05/16/1994", + birthday_notification_id: UUID(), + email: "ryantoken13@gmail.com", + id: UUID(), + name: "Ryan Token", + next_notification_date_time: "", + notification_identifier: UUID(), + notification_preference: 0, + notification_preference_custom_day: 3, + notification_preference_custom_month: 0, + notification_preference_custom_year: 0, + notification_preference_hour: 12, + notification_preference_minute: 0, + notification_preference_weekday: 3, + notification_preference_week_of_month: 2, + phone: "6363687771", + picture: "photo-as-data-string", + secondary_address: "", + secondary_email: "", + secondary_phone: "", + unread_badge_date_time: "" + ) +} diff --git a/CatchUp-SwiftUI/DetailScreen.swift b/CatchUp-SwiftUI/DetailScreen.swift deleted file mode 100644 index 772ab37..0000000 --- a/CatchUp-SwiftUI/DetailScreen.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// DetailScreen.swift -// CatchUp-SwiftUI -// -// Created by Ryan Token on 4/11/20. -// Copyright © 2020 Token Solutions. All rights reserved. -// - -import SwiftUI - -struct DetailScreen: View { - @State private var showingPreferenceScreen = false - @Environment(\.managedObjectContext) var managedObjectContext - - let notificationService = NotificationService() - let converter = Conversions() - let helper = GeneralHelpers() - let contactService = ContactService() - - let contact: SelectedContact - - var body: some View { - VStack { - GradientView() - .edgesIgnoringSafeArea(.top) - .frame(height: 75) - - ContactPhoto(image: self.converter.getContactPicture(from: contact.picture)) - .offset(x: 0, y: -130) - .padding(.bottom, -130) - - VStack(alignment: .center, spacing: 10) { - Text(contact.name) - .font(.largeTitle) - .bold() - HStack(spacing: 0) { - Text("Preference: ") - .foregroundColor(.gray) - Text(converter.convertNotificationPreferenceIntToString(preference: Int(contact.notification_preference), contact: contact)) - .foregroundColor(.gray) - } - Button(action: { - self.showingPreferenceScreen = true - }) { - Text("Change Notification Preference") - .font(.headline) - .foregroundColor(.orange) - } - } - - List { - Section(header: Text("Contact Information")) { - if contactService.contactHasPhone(contact) { - VStack(alignment: .leading, spacing: 3) { - Text("Phone") - .font(.caption) - - Button(converter.getFormattedPhoneNumber(from: contact.phone)) { - UIApplication.shared.open(self.converter.getTappablePhoneNumber(from: self.contact.phone)) - } - .foregroundColor(.blue) - } - } - if contactService.contactHasSecondaryPhone(contact) { - VStack(alignment: .leading, spacing: 3) { - Text("Secondary Phone") - .font(.caption) - - Button(converter.getFormattedPhoneNumber(from: contact.secondary_phone)) { - UIApplication.shared.open(self.converter.getTappablePhoneNumber(from: self.contact.secondary_phone)) - } - .foregroundColor(.blue) - } - } - if contactService.contactHasEmail(contact) { - VStack(alignment: .leading, spacing: 3) { - Text("Email") - .font(.caption) - - Button(contact.email) { - UIApplication.shared.open(self.converter.getTappableEmail(from: self.contact.email)) - } - .foregroundColor(.blue) - } - } - if contactService.contactHasSecondaryEmail(contact) { - VStack(alignment: .leading, spacing: 3) { - Text("Secondary Email") - .font(.caption) - - Button(contact.secondary_email) { - UIApplication.shared.open(self.converter.getTappableEmail(from: self.contact.secondary_email)) - } - .foregroundColor(.blue) - } - } - if contactService.contactHasAddress(contact) { - VStack(alignment: .leading, spacing: 3) { - Text("Address") - .font(.caption) - Text(contact.address) - } - } - if contactService.contactHasSecondaryAddress(contact) { - VStack(alignment: .leading, spacing: 3) { - Text("Secondary Address") - .font(.caption) - Text(contact.secondary_address) - } - } - if notificationService.contactHasBirthday(contact) { - VStack(alignment: .leading, spacing: 3) { - Text("Birthday") - .font(.caption) - Text(converter.getFormattedBirthdayOrAnniversary(from: contact.birthday)) - } - } - if notificationService.contactHasAnniversary(contact) { - VStack(alignment: .leading, spacing: 3) { - Text("Anniversary") - .font(.caption) - Text(converter.getFormattedBirthdayOrAnniversary(from: contact.anniversary)) - } - } - } - } - } - .sheet( - isPresented: $showingPreferenceScreen, - onDismiss: { - self.notificationService.removeExistingNotifications(for: self.contact) - self.notificationService.createNewNotification(for: self.contact, moc: self.managedObjectContext) - }) { - - // the fact that I have to manually pass in the MOC is dumb - // hopefully this is a SwiftUI v1 bug that's fixed at WWDC this year - // (https://stackoverflow.com/questions/58328201/saving-core-data-entity-in-popover-in-swiftui-throws-nilerror-without-passing-e) - PreferenceScreen(contact: self.contact).environment(\.managedObjectContext, self.managedObjectContext) - } - .onAppear(perform: helper.clearNotificationBadge) - } -} diff --git a/CatchUp-SwiftUI/Extensions/ModelContext+sqliteCommand.swift b/CatchUp-SwiftUI/Extensions/ModelContext+sqliteCommand.swift new file mode 100644 index 0000000..4e0f44c --- /dev/null +++ b/CatchUp-SwiftUI/Extensions/ModelContext+sqliteCommand.swift @@ -0,0 +1,19 @@ +// +// ModelContext+sqliteCommand.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/3/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import SwiftData + +extension ModelContext { + var sqliteCommand: String { + if let url = container.configurations.first?.url.path(percentEncoded: false) { + "sqlite3 \"\(url)\"" + } else { + "No SQLite database found." + } + } +} diff --git a/CatchUp-SwiftUI/Extensions/UserDefaults+isFirstLaunch.swift b/CatchUp-SwiftUI/Extensions/UserDefaults+isFirstLaunch.swift deleted file mode 100644 index c06411d..0000000 --- a/CatchUp-SwiftUI/Extensions/UserDefaults+isFirstLaunch.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// UserDefaults+isFirstLaunch.swift -// CatchUp-SwiftUI -// -// Created by Ryan Token on 4/19/20. -// Copyright © 2020 Token Solutions. All rights reserved. -// - -import Foundation - -extension UserDefaults { - // check for is first launch on version 2.0 - only true on first invocation after app install, false on all further invocations - // Note: this is used in AppDelegate.swift in didFinishLaunchingWithOptions - static func isFirstVersion2Launch() -> Bool { - let hasLaunchedVersion2BeforeFlag = "hasLaunchedVersion2BeforeFlag" - let isFirstVersion2Launch = !UserDefaults.standard.bool(forKey: hasLaunchedVersion2BeforeFlag) - if (isFirstVersion2Launch) { - UserDefaults.standard.set(true, forKey: hasLaunchedVersion2BeforeFlag) - UserDefaults.standard.synchronize() - } - isFirstVersion2Launch ? print("It is the first launch on version 2.0") : print("It is not the first launch on version 2.0") - return isFirstVersion2Launch - } -} diff --git a/CatchUp-SwiftUI/Extensions/View+if.swift b/CatchUp-SwiftUI/Extensions/View+if.swift new file mode 100644 index 0000000..550e77e --- /dev/null +++ b/CatchUp-SwiftUI/Extensions/View+if.swift @@ -0,0 +1,24 @@ +// +// View+if.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/24/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import SwiftUI + +extension View { + /// Applies the given transform if the given condition evaluates to `true`. + /// - Parameters: + /// - condition: The condition to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. + @ViewBuilder func `if`(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View { + if condition() { + transform(self) + } else { + self + } + } +} diff --git a/CatchUp-SwiftUI/HomeScreen.swift b/CatchUp-SwiftUI/HomeScreen.swift deleted file mode 100644 index b01caed..0000000 --- a/CatchUp-SwiftUI/HomeScreen.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// ContentView.swift -// CatchUp-SwiftUI -// -// Created by Ryan Token on 6/26/19. -// Copyright © 2019 Token Solutions. All rights reserved. -// - -import SwiftUI -import SwiftUIKit -import ContactsUI - -enum ActiveSheet: Identifiable { - case contactPicker - case about - case updates - - var id: UUID { - UUID() - } -} - -struct HomeScreen : View { - @State private var showSheet = false - @State private var showContactPicker = false - @State private var contacts: [CNContact] = [] - @State private var activeSheet: ActiveSheet? - @State private var showUpdates: ActiveSheet = .updates - @Environment(\.managedObjectContext) var managedObjectContext - @FetchRequest(entity: SelectedContact.entity(), - sortDescriptors: []) var selectedContacts: FetchedResults - - let notificationService = NotificationService() - let helper = GeneralHelpers() - let converter = Conversions() - let contactHelper = ContactHelper() - - init() { - //Use this if NavigationBarTitle is with Large Font - UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.systemOrange] - } - - var body: some View { - NavigationView { - VStack { - List { - ForEach(selectedContacts) { contact in - NavigationLink(destination: DetailScreen(contact: contact)) { - HStack { - converter.getContactPicture(from: contact.picture) - .renderingMode(.original) - .resizable() - .frame(width: 45, height: 45, alignment: .leading) - .clipShape(Circle()) - - VStack(alignment: .leading, spacing: 2) { - Text(contact.name) - .font(.headline) - Text(converter.convertNotificationPreferenceIntToString(preference: Int(contact.notification_preference), contact: contact)) - .font(.caption) - .foregroundColor(.gray) - } - } - } - } - .onDelete(perform: removePendingNotificationsAndDeleteContact) - } - - Button(action: { - activeSheet = .contactPicker - showContactPicker.toggle() - }) { - HStack(alignment: .center, spacing: 6) { - Image(systemName: "person.crop.circle.fill.badge.plus") - - Text("Add Contacts") - } - .font(.headline) - .foregroundColor(.blue) - .padding(.top) - .padding(.bottom) - } - - .navigationBarTitle(Text("CatchUp")) - - .navigationBarItems(trailing: - Button(action: { - activeSheet = .about - }) { - Image(systemName: "ellipsis.circle") - .font(.title2) - .foregroundColor(.blue) - } - ) - } - - .sheet(item: $activeSheet, onDismiss: { activeSheet = nil }) { item in - switch item { - case .contactPicker: - ContactPicker( - showPicker: $showContactPicker, - onSelectContacts: { c in - contacts = c - contactHelper.saveSelectedContact(for: contacts) - } - ) - case .about: - AboutScreen() - case .updates: - UpdatesScreen() - } - } - } - .accentColor(.orange) - .onAppear(perform: clearNotificationBadgeAndCheckForUpdate) - } - - func clearNotificationBadgeAndCheckForUpdate() { - fetchAvailableIAPs() - helper.clearNotificationBadge() - helper.saveMOC(moc: managedObjectContext) - checkForUpdate() - } - - func removePendingNotificationsAndDeleteContact(at offsets: IndexSet) { - for index in offsets { - let contact = selectedContacts[index] - - notificationService.removeExistingNotifications(for: contact) - managedObjectContext.delete(contact) - } - } - - func checkForUpdate() { - let version = helper.getCurrentAppVersion() - let savedVersion = UserDefaults.standard.string(forKey: "savedVersion") - - if savedVersion == version { - print("App is up to date!") - } else { - if updateIsMajor() { - // Toggle to show UpdatesScreen as a sheet - print("Major update detected, showing UpdatesScreen...") - activeSheet = .updates - } - UserDefaults.standard.set(version, forKey: "savedVersion") - } - } - - func updateIsMajor() -> Bool { - let version = helper.getCurrentAppVersion() - if version.suffix(2) == ".0" { - return true - } else { - return false - } - } - - func fetchAvailableIAPs() { - print("fetching IAPs") - IAPService.shared.fetchAvailableProducts() - } -} - -struct HomeScreen_Previews : PreviewProvider { - static var previews: some View { - - let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext - - return HomeScreen().environment(\.managedObjectContext, context) - } -} diff --git a/CatchUp-SwiftUI/Info.plist b/CatchUp-SwiftUI/Info.plist index 695e4eb..dc118c6 100644 --- a/CatchUp-SwiftUI/Info.plist +++ b/CatchUp-SwiftUI/Info.plist @@ -2,65 +2,35 @@ - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 CFBundleName $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) - LSApplicationCategoryType - public.app-category.social-networking - LSRequiresIPhoneOS - + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) NSContactsUsageDescription CatchUp needs access to your Contacts in order to remind you to catch up with the people you choose. CatchUp does not store or save anything. - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UILaunchStoryboardName - LaunchScreen - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities + UIBackgroundModes - armv7 + remote-notification + UILaunchScreen + + UIColorName + LaunchScreenBackgroundColor + UISupportedInterfaceOrientations - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeRight UIInterfaceOrientationLandscapeLeft - - UISupportedInterfaceOrientations~ipad - + UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight diff --git a/CatchUp-SwiftUI/PreferenceScreen.swift b/CatchUp-SwiftUI/PreferenceScreen.swift deleted file mode 100644 index 0ed886a..0000000 --- a/CatchUp-SwiftUI/PreferenceScreen.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// PreferenceScreen.swift -// CatchUp-SwiftUI -// -// Created by Ryan Token on 4/11/20. -// Copyright © 2020 Token Solutions. All rights reserved. -// - -import SwiftUI - -struct PreferenceScreen: View { - @State private var notificationPreference: Int - @State private var notificationPreferenceTime: Date - @State private var notificationPreferenceWeekday: Int - @State private var notificationPreferenceCustomDate: Date - @Environment(\.presentationMode) var presentationMode - @Environment(\.managedObjectContext) var managedObjectContext - - let notificationService = NotificationService() - let now = Date() - - var contact: SelectedContact - var notificationOptions = ["Never", "Daily", "Weekly", "Monthly", "Custom"] - var dayOptions = ["Sun", "Mon", "Tues", "Wed", "Thur", "Fri", "Sat"] - - // set default values equal to their Core Data values for new contacts who haven't been changed yet - // many of these defaults are set in ContactPickerViewController.swift - init(contact: SelectedContact) { - - let calendar = Calendar.current - let timeComponents = DateComponents(calendar: calendar, hour: Int(contact.notification_preference_hour), minute: Int(contact.notification_preference_minute)) - let time = Calendar.current.date(from: timeComponents) - - let customDateComponents = DateComponents(calendar: calendar, year: Int(contact.notification_preference_custom_year), month: Int(contact.notification_preference_custom_month), day: Int(contact.notification_preference_custom_day)) - let customDate = Calendar.current.date(from: customDateComponents) - - self.contact = contact - self._notificationPreference = State(initialValue: Int(contact.notification_preference)) - self._notificationPreferenceTime = State(initialValue: time!) - self._notificationPreferenceWeekday = State(initialValue: Int(contact.notification_preference_weekday)) - self._notificationPreferenceCustomDate = State(initialValue: customDate!) - } - - var body: some View { - VStack(alignment: .leading, spacing: 10) { - HStack { - Spacer() - Button("Save") { - self.presentationMode.wrappedValue.dismiss() - } - .foregroundColor(.blue) - .font(.headline) - .padding(.top) - .padding(.trailing) - } - - Text("Preference") - .font(.largeTitle) - .bold() - .foregroundColor(.orange) - .padding(.bottom) - - Text("How often should we notify you to CatchUp with \(contact.name)?") - - Picker(selection: $notificationPreference, label: Text("How often should we notify you to CatchUp with \(contact.name)?")) { - ForEach(0.. Bool { - return contact.phone != "" ? true : false - } - - func contactHasSecondaryPhone(_ contact: SelectedContact) -> Bool { - return contact.secondary_phone != "" ? true : false - } - - func contactHasEmail(_ contact: SelectedContact) -> Bool { - return contact.email != "" ? true : false - } - - func contactHasSecondaryEmail(_ contact: SelectedContact) -> Bool { - return contact.secondary_email != "" ? true : false - } - - func contactHasAddress(_ contact: SelectedContact) -> Bool { - return contact.address != "" ? true : false - } - - func contactHasSecondaryAddress(_ contact: SelectedContact) -> Bool { - return contact.secondary_address != "" ? true : false - } - - // MARK: Functions for ContactPickerViewController - - func encodeContactPicture(for contact: CNContact) -> String { - let picture: String - - if contact.imageDataAvailable == true { - let dataPicture: NSData = contact.thumbnailImageData! as NSData - picture = dataPicture.base64EncodedString() - } else { - // there is not an image for this contact, use the default image - let image = UIImage(named: "DefaultPhoto") - let imageData: NSData = image!.pngData()! as NSData - picture = imageData.base64EncodedString() - } - - return picture - } - - func getContactName(for contact: CNContact) -> String { - var name:String - - //if they have a first and a last name - if contact.givenName != "" && contact.familyName != "" { - name = contact.givenName + " " + contact.familyName - //if they have a first name, but no last name - } else if contact.givenName != "" && contact.familyName == "" { - name = contact.givenName - //if they have no first name, but have a last name - } else if contact.givenName == "" && contact.familyName != "" { - name = contact.familyName - } else { - name = "" - } - - return name - } - - func getContactPrimaryPhone(for contact: CNContact) -> String { - let userPhoneNumbers: [CNLabeledValue] = contact.phoneNumbers - var phone: String - - //check for phone numbers and set values - if userPhoneNumbers.count > 0 { - let firstPhoneNumber = userPhoneNumbers[0].value - phone = firstPhoneNumber.stringValue - } else { - phone = "" - } - - return phone - } - - func getContactSecondaryPhone(for contact: CNContact) -> String { - let userPhoneNumbers: [CNLabeledValue] = contact.phoneNumbers - var secondary_phone: String - - if userPhoneNumbers.count > 1 { - let secondPhoneNumber = userPhoneNumbers[1].value - secondary_phone = secondPhoneNumber.stringValue - } else { - secondary_phone = "" - } - - return secondary_phone - } - - func getContactPrimaryEmail(for contact: CNContact) -> String { - let emailAddresses = contact.emailAddresses - var email: String - - if emailAddresses.count > 0 { - let firstEmail = emailAddresses[0].value - email = firstEmail as String - } else { - email = "" - } - - return email - } - - func getContactSecondaryEmail(for contact: CNContact) -> String { - let emailAddresses = contact.emailAddresses - var secondary_email: String - - if emailAddresses.count > 1 { - let secondEmail = emailAddresses[1].value - secondary_email = secondEmail as String - } else { - secondary_email = "" - } - - return secondary_email - } - - func getContactPrimaryAddress(for contact: CNContact) -> String { - //contact postal address array - let addresses = contact.postalAddresses - var address: String - - //check for postal addresses and set values - if addresses.count > 0 { - let firstAddress = addresses[0].value - let fullAddress = firstAddress.street + ", " + firstAddress.city + ", " + firstAddress.state + " " + firstAddress.postalCode - address = fullAddress.replacingOccurrences(of: "\n", with: " ") - } else { - address = "" - } - - return address - } - - func getContactSecondaryAddress(for contact: CNContact) -> String { - //contact postal address array - let addresses = contact.postalAddresses - var secondary_address: String - - //check for postal addresses and set values - if addresses.count > 1 { - let secondAddress = addresses[1].value - let fullAddress = secondAddress.street + ", " + secondAddress.city + ", " + secondAddress.state + " " + secondAddress.postalCode - secondary_address = fullAddress.replacingOccurrences(of: "\n", with: " ") - } else { - secondary_address = "" - } - - return secondary_address - } - - func getContactBirthday(for contact: CNContact) -> String { - var birthdayString: String - - if contact.birthday != nil { - - let birthday = contact.birthday?.date - - let formatter = DateFormatter() - formatter.dateFormat = "MM-dd" - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - - birthdayString = formatter.string(from: birthday!) - - let birthdayDate = formatter.date(from: birthdayString)! - birthdayString = formatter.string(from: birthdayDate) - - } else { - birthdayString = "" - } - - return birthdayString - } - - func getContactAnniversary(for contact: CNContact) -> String { - //check for anniversary and set value for anniversary and reminder preference - var anniversaryString: String - - let anniversary = contact.dates.filter { date -> Bool in - - guard let label = date.label else { - return false - } - - return label.contains("Anniversary") - - } .first?.value as DateComponents? - - if anniversary?.date != nil { - - let formatter = DateFormatter() - formatter.dateFormat = "MM-dd" - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - - anniversaryString = formatter.string(from: (anniversary?.date!)!) - - let anniversaryDate = formatter.date(from: anniversaryString)! - anniversaryString = formatter.string(from: anniversaryDate) - - } else { - anniversaryString = "" - } - - return anniversaryString - } - -} diff --git a/CatchUp-SwiftUI/Services/IAPService.swift b/CatchUp-SwiftUI/Services/IAPService.swift index 841785c..7e64982 100644 --- a/CatchUp-SwiftUI/Services/IAPService.swift +++ b/CatchUp-SwiftUI/Services/IAPService.swift @@ -23,7 +23,7 @@ enum IAPServiceAlertType{ } } -class IAPService: NSObject { +final class IAPService: NSObject { static let shared = IAPService() let graciousTipProductID = "gracious_tip_0.99" @@ -39,13 +39,13 @@ class IAPService: NSObject { // MARK: - MAKE PURCHASE OF A PRODUCT func canMakePurchases() -> Bool { return SKPaymentQueue.canMakePayments() } - func leaveATip(index: Int){ + func leaveATip(index: Int) { if iapProducts.count == 0 { print("No IAPs to purchase") return } - if self.canMakePurchases() { + if canMakePurchases() { let product = iapProducts[index] let payment = SKPayment(product: product) SKPaymentQueue.default().add(self) @@ -59,27 +59,15 @@ class IAPService: NSObject { } func getSmallTipAmount() -> String { - if iapProducts.count > 0 { - return iapProducts[1].localizedPrice - } else { - return "$0.99" - } + return iapProducts.first(where: { $0.productIdentifier == "gracious_tip_0.99" })?.localizedPrice ?? "$0.99" } func getMediumTipAmount() -> String { - if iapProducts.count > 0 { - return iapProducts[0].localizedPrice - } else { - return "$1.99" - } + return iapProducts.first(where: { $0.productIdentifier == "generous_tip_1.99" })?.localizedPrice ?? "$1.99" } func getLargeTipAmount() -> String { - if iapProducts.count > 0 { - return iapProducts[2].localizedPrice - } else { - return "$4.99" - } + return iapProducts.first(where: { $0.productIdentifier == "gratuitous_tip_4.99" })?.localizedPrice ?? "$4.99" } // MARK: - RESTORE PURCHASE @@ -103,9 +91,10 @@ class IAPService: NSObject { extension IAPService: SKProductsRequestDelegate, SKPaymentTransactionObserver{ // MARK: - REQUEST IAP PRODUCTS func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) { - if (response.products.count > 0) { - iapProducts = response.products + let sortedProducts = response.products.sorted(by: { $0.price.decimalValue < $1.price.decimalValue }) + + iapProducts = sortedProducts for product in iapProducts{ let numberFormatter = NumberFormatter() numberFormatter.formatterBehavior = .behavior10_4 diff --git a/CatchUp-SwiftUI/Services/NotificationService.swift b/CatchUp-SwiftUI/Services/NotificationService.swift deleted file mode 100644 index b843590..0000000 --- a/CatchUp-SwiftUI/Services/NotificationService.swift +++ /dev/null @@ -1,317 +0,0 @@ -// -// NotificationService.swift -// CatchUp-SwiftUI -// -// Created by Ryan Token on 4/21/20. -// Copyright © 2020 Token Solutions. All rights reserved. -// - -import SwiftUI -import UserNotifications -import CoreData - -struct NotificationService { - let notificationCenter = UNUserNotificationCenter.current() - let helper = GeneralHelpers() - let contactService = ContactService() - - func createNewNotification(for contact: SelectedContact, moc: NSManagedObjectContext) { - let addRequest = { - if self.preferenceIsNotSetToNever(for: contact) { - self.addGeneralNotification(for: contact, moc: moc) - } - - if self.contactHasBirthday(contact) { - self.addBirthdayNotification(for: contact, moc: moc) - } - - if self.contactHasAnniversary(contact) { - self.addAnniversaryNotification(for: contact, moc: moc) - } - } - - checkNotificationAuthorizationStatusAndAddRequest(action: addRequest) - } - - func preferenceIsNotSetToNever(for contact: SelectedContact) -> Bool { - return contact.notification_preference != 0 ? true : false - } - - func contactHasBirthday(_ contact: SelectedContact) -> Bool { - return contact.birthday != "" ? true : false - } - - func contactHasAnniversary(_ contact: SelectedContact) -> Bool { - return contact.anniversary != "" ? true : false - } - - func addGeneralNotification(for contact: SelectedContact, moc: NSManagedObjectContext) { - let notificationContent = UNMutableNotificationContent() - notificationContent.title = "👋 CatchUp with \(contact.name)" - notificationContent.body = self.generateRandomNotificationBody() - notificationContent.sound = UNNotificationSound.default - notificationContent.badge = 1 - - let identifier = UUID() - let dateComponents = self.setNotificationDateComponents(for: contact) - - self.scheduleNotification(for: contact, dateComponents: dateComponents, identifier: identifier, content: notificationContent, moc: moc) - } - - func addBirthdayNotification(for contact: SelectedContact, moc: NSManagedObjectContext) { - let birthdayNotificationContent = UNMutableNotificationContent() - birthdayNotificationContent.title = "🥳 Today is \(contact.name)'s birthday!" - birthdayNotificationContent.body = "Be sure to CatchUp and wish them a great one!" - birthdayNotificationContent.sound = UNNotificationSound.default - birthdayNotificationContent.badge = 1 - - let birthdayIdentifier = UUID() - let birthdayDateComponents = self.setBirthdayDateComponents(for: contact) - - self.scheduleNotification(for: contact, dateComponents: birthdayDateComponents, identifier: birthdayIdentifier, content: birthdayNotificationContent, moc: moc) - } - - func addAnniversaryNotification(for contact: SelectedContact, moc: NSManagedObjectContext) { - let anniversaryNotificationContent = UNMutableNotificationContent() - anniversaryNotificationContent.title = "😍 Tomorrow is \(contact.name)'s anniversary!" - anniversaryNotificationContent.body = "Be sure to CatchUp and wish them the best." - anniversaryNotificationContent.sound = UNNotificationSound.default - anniversaryNotificationContent.badge = 1 - - let anniversaryIdentifier = UUID() - let anniversaryDateComponents = self.setAnniversaryDateComponents(for: contact) - - self.scheduleNotification(for: contact, dateComponents: anniversaryDateComponents, identifier: anniversaryIdentifier, content: anniversaryNotificationContent, moc: moc) - } - - func checkNotificationAuthorizationStatusAndAddRequest(action: @escaping() -> Void) { - notificationCenter.getNotificationSettings { settings in - if settings.authorizationStatus == .authorized { - action() - print("Scheduled new notification(s)") - } else { - self.notificationCenter.requestAuthorization(options: [.alert, .badge, .sound]) { success, error in - if success { - action() - } else { - print("User isn't allowing notifications :(") - } - } - } - } - } - - func setNotificationDateComponents(for contact: SelectedContact) -> DateComponents { - var dateComponents = DateComponents() - - switch contact.notification_preference { - case 0: // Never - break - case 1: // Daily - dateComponents.hour = Int(contact.notification_preference_hour) - dateComponents.minute = Int(contact.notification_preference_minute) - break - case 2: // Weekly - dateComponents.hour = Int(contact.notification_preference_hour) - dateComponents.minute = Int(contact.notification_preference_minute) - // weekday units are 1-7, I store them as 0-6 though. Need to add 1 - dateComponents.weekday = Int(contact.notification_preference_weekday)+1 - break - case 3: // Monthly - dateComponents.hour = Int(contact.notification_preference_hour) - dateComponents.minute = Int(contact.notification_preference_minute) - dateComponents.weekday = Int(contact.notification_preference_weekday)+1 - dateComponents.weekOfMonth = Int.random(in: 2..<5) - break - case 4: // Custom Date - dateComponents.month = Int(contact.notification_preference_custom_month) - dateComponents.day = Int(contact.notification_preference_custom_day) - dateComponents.year = Int(contact.notification_preference_custom_year) - dateComponents.hour = 12 - dateComponents.minute = 30 - break - default: - print("It's impossible to get here") - } - - return dateComponents - } - - func setBirthdayDateComponents(for contact: SelectedContact) -> DateComponents { - var birthdayDateComponents = DateComponents() - - let month = (contact.birthday).prefix(2) - let day = (contact.birthday).suffix(2) - - birthdayDateComponents.month = Int(month) - birthdayDateComponents.day = Int(day) - birthdayDateComponents.hour = 7 - birthdayDateComponents.minute = 15 - - return birthdayDateComponents - } - - func setAnniversaryDateComponents(for contact: SelectedContact) -> DateComponents { - var anniversaryDateComponents = DateComponents() - let formatter = DateFormatter() - - formatter.dateFormat = "MM-dd" - let anniversaryDate = formatter.date(from: contact.anniversary)! - let previousDayDate = Calendar.current.date(byAdding: .day, value: -1, to: anniversaryDate) - let previousDay = formatter.string(from: previousDayDate!) - - let month = (previousDay).prefix(2) - let day = (previousDay).suffix(2) - - anniversaryDateComponents.month = Int(month) - anniversaryDateComponents.day = Int(day) - anniversaryDateComponents.hour = 7 - anniversaryDateComponents.minute = 30 - - return anniversaryDateComponents - } - - func scheduleNotification(for contact: SelectedContact, dateComponents: DateComponents, identifier: UUID, content: UNMutableNotificationContent, moc: NSManagedObjectContext) { - - let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true) - let request = UNNotificationRequest(identifier: identifier.uuidString, content: content, trigger: trigger) - - moc.performAndWait { - if content.title == "👋 CatchUp with \(contact.name)" { - contact.notification_identifier = identifier - } else if content.title == "🥳 Today is \(contact.name)'s birthday!" { - contact.birthday_notification_id = identifier - } else { - contact.anniversary_notification_id = identifier - } - } - self.helper.saveMOC(moc: moc) - - notificationCenter.add(request) - } - - func generateRandomNotificationBody() -> String { - let randomInt = Int.random(in: 0..<20) - - switch randomInt { - case 0: - return "Now you can be best buddies again" - case 1: - return "A little birdy told me they really miss you" - case 2: - return "It's time to check back in" - case 3: - return "You're a good friend. Good for you. Tell this person to get CatchUp too so it's not always you who's reaching out" - case 4: - return "Today is the perfect day to get back in touch" - case 5: - return "Remember to keep in touch with the people that matter most" - case 6: - return "You know what they say: 'A CatchUp a day keeps the needy friends at bay'" - case 7: - return "Have you written a physical letter in a while? Maybe give that a try this time. People like that" - case 8: - return "Here's that reminder you set to check in with someone important. Maybe you'll make their day" - case 9: - return "Once a good person, always a good person (you are a good person, and probably so is the person you want to be reminded to CatchUp with)" - case 10: - return "Here's another reminder to get back in touch with ⬆️" - case 11: - return "Time to get back in contact with one of your favorite people" - case 12: - return "So nice of you to want to stay in touch with the people you care about" - case 13: - return "You know they'll really appreciate it" - case 14: - return "I'm not guilting you into this or anything, but this person will probably be sad if you don't say hello." - case 15: - return "You have this app, so you must be cool and nice. Now send a thoughtful message to this also cool and nice person" - case 16: - return "Once upon a time, there was a nice person. The end. (Spoiler: you're the nice person - keep being nice and reach out to your friend)" - case 17: - return "Another timely reminder to catch up. Just the way you wanted it" - case 18: - return "Now is the time! Seize the moment!" - case 19: - return "Good job keeping your friends close. Now keep your enemies closer 😉" - default: - return "Keep in touch" - } - } - - func updateNotificationPreference(for contact: SelectedContact, selection: Int, moc: NSManagedObjectContext) { - let newPreference = selection - moc.performAndWait { - contact.notification_preference = Int16(newPreference) - } - - helper.saveMOC(moc: moc) - } - - func updateNotificationTime(for contact: SelectedContact, hour: Int, minute: Int, moc: NSManagedObjectContext) { - let newHour = hour - let newMinute = minute - moc.performAndWait { - contact.notification_preference_hour = Int16(newHour) - contact.notification_preference_minute = Int16(newMinute) - } - - helper.saveMOC(moc: moc) - } - - func updateNotificationPreferenceWeekday(for contact: SelectedContact, weekday: Int, moc: NSManagedObjectContext) { - let newWeekday = weekday - moc.performAndWait { - contact.notification_preference_weekday = Int16(newWeekday) - } - - helper.saveMOC(moc: moc) - } - - func updateNotificationCustomDate(for contact: SelectedContact, month: Int, day: Int, year: Int, moc: NSManagedObjectContext) { - let customMonth = month - let customDay = day - let customYear = year - moc.performAndWait { - contact.notification_preference_custom_month = Int16(customMonth) - contact.notification_preference_custom_day = Int16(customDay) - contact.notification_preference_custom_year = Int16(customYear) - } - - helper.saveMOC(moc: moc) - } - - func requestAuthorizationForNotifications() { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in - if success { - print("User authorized CatchUp to send notifications") - } else if let error = error { - print(error.localizedDescription) - } - } - } - - func removeExistingNotifications(for contact: SelectedContact) { - removeGeneralNotification(for: contact) - - if contactHasBirthday(contact) { - removeBirthdayNotification(for: contact) - } - - if contactHasAnniversary(contact) { - removeAnniversaryNotification(for: contact) - } - } - - func removeGeneralNotification(for contact: SelectedContact) { - UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [contact.notification_identifier.uuidString]) - } - - func removeBirthdayNotification(for contact: SelectedContact) { - UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [contact.birthday_notification_id.uuidString]) - } - - func removeAnniversaryNotification(for contact: SelectedContact) { - UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [contact.anniversary_notification_id.uuidString]) - } -} diff --git a/CatchUp-SwiftUI/UpdatesScreen.swift b/CatchUp-SwiftUI/UpdatesScreen.swift deleted file mode 100644 index b050647..0000000 --- a/CatchUp-SwiftUI/UpdatesScreen.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// UpdatesScreen.swift -// CatchUp-SwiftUI -// -// Created by Ryan Token on 4/29/20. -// Copyright © 2020 Token Solutions. All rights reserved. -// - -import SwiftUI - -struct UpdatesScreen: View { - let helper = GeneralHelpers() - - var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 10) { - Group { - - Spacer() - .frame(height: 45) - - Text("New Update") - .font(.largeTitle) - .bold() - .foregroundColor(.orange) - - Text("Version \(helper.getCurrentAppVersion())") - .font(.headline) - .foregroundColor(.blue) - - - Text("Release Notes:") - .font(.headline) - - Divider() - Spacer() - - } - - Group { - Text("– iOS 15 compatibility") - - Spacer() - - Text("– CatchUp now requires iOS 14 or newer") - - Spacer() - - Text("– More to come soon!") - } - - Spacer() - } - } - .padding([.top, .horizontal]) - } -} - -struct UpdatesScreen_Previews: PreviewProvider { - @Environment(\.presentationMode) var presentationMode - - static var previews: some View { - UpdatesScreen() - } -} diff --git a/CatchUp-SwiftUI/Utilities/ContactHelper.swift b/CatchUp-SwiftUI/Utilities/ContactHelper.swift index 7bd0d29..f52458c 100644 --- a/CatchUp-SwiftUI/Utilities/ContactHelper.swift +++ b/CatchUp-SwiftUI/Utilities/ContactHelper.swift @@ -2,113 +2,396 @@ // ContactHelper.swift // CatchUp-SwiftUI // -// Created by Ryan Token on 11/15/20. +// Created by Ryan Token on 4/11/20. // Copyright © 2020 Token Solutions. All rights reserved. // -import Foundation -import ContactsUI +import SwiftUI +import UIKit +import Contacts import CoreData struct ContactHelper { - // save selected contacts and their properties to Core Data - func saveSelectedContact(for contacts: [CNContact]) { - print("saving...") - - let contactService = ContactService() - for contact in contacts { - let currentMinute = Calendar.current.component(.minute, from: Date()) - let currentHour = Calendar.current.component(.hour, from: Date()) - let currentDay = Calendar.current.component(.day, from: Date()) - let currentMonth = Calendar.current.component(.month, from: Date()) - let currentYear = Calendar.current.component(.year, from: Date()) - - let id = UUID() - let address = contactService.getContactPrimaryAddress(for: contact) - let anniversary = contactService.getContactAnniversary(for: contact) - let anniversary_notification_ID = UUID() - let birthday = contactService.getContactBirthday(for: contact) - let birthday_notification_ID = UUID() - let email = contactService.getContactPrimaryEmail(for: contact) - let name = contactService.getContactName(for: contact) - let notification_identifier = UUID() - let notification_preference = 0 - let notification_preference_hour = currentHour - let notification_preference_minute = currentMinute - let notification_preference_weekday = 0 - let notification_preference_custom_year = currentYear - let notification_preference_custom_month = currentMonth - let notification_preference_custom_day = currentDay - let phone = contactService.getContactPrimaryPhone(for: contact) - let picture = contactService.encodeContactPicture(for: contact) - let secondary_email = contactService.getContactSecondaryEmail(for: contact) - let secondary_address = contactService.getContactSecondaryAddress(for: contact) - let secondary_phone = contactService.getContactSecondaryPhone(for: contact) + // MARK: Functions for DetailScreen + + static func contactHasPhone(_ contact: SelectedContact) -> Bool { + return contact.phone != "" ? true : false + } + + static func contactHasSecondaryPhone(_ contact: SelectedContact) -> Bool { + return contact.secondary_phone != "" ? true : false + } + + static func contactHasEmail(_ contact: SelectedContact) -> Bool { + return contact.email != "" ? true : false + } + + static func contactHasSecondaryEmail(_ contact: SelectedContact) -> Bool { + return contact.secondary_email != "" ? true : false + } + + static func contactHasAddress(_ contact: SelectedContact) -> Bool { + return contact.address != "" ? true : false + } + + static func contactHasSecondaryAddress(_ contact: SelectedContact) -> Bool { + return contact.secondary_address != "" ? true : false + } + + static func contactHasBirthday(_ contact: SelectedContact) -> Bool { + return contact.birthday != "" ? true : false + } + + static func contactHasAnniversary(_ contact: SelectedContact) -> Bool { + return contact.anniversary != "" ? true : false + } + + // MARK: Functions for ContactPickerViewController + + static func encodeContactPicture(for contact: CNContact) -> String { + let picture: String + + if contact.imageDataAvailable == true { + let dataPicture: NSData = contact.thumbnailImageData! as NSData + picture = dataPicture.base64EncodedString() + } else { + // there is not an image for this contact, use the default image + let image = UIImage(named: "DefaultPhoto") + let imageData: NSData = image!.pngData()! as NSData + picture = imageData.base64EncodedString() + } + + return picture + } + + static func getContactName(for contact: CNContact) -> String { + var name:String + + //if they have a first and a last name + if contact.givenName != "" && contact.familyName != "" { + name = contact.givenName + " " + contact.familyName + //if they have a first name, but no last name + } else if contact.givenName != "" && contact.familyName == "" { + name = contact.givenName + //if they have no first name, but have a last name + } else if contact.givenName == "" && contact.familyName != "" { + name = contact.familyName + } else { + name = "" + } + + return name + } + + static func getContactPrimaryPhone(for contact: CNContact) -> String { + let userPhoneNumbers: [CNLabeledValue] = contact.phoneNumbers + var phone: String + + //check for phone numbers and set values + if userPhoneNumbers.count > 0 { + let firstPhoneNumber = userPhoneNumbers[0].value + phone = firstPhoneNumber.stringValue + } else { + phone = "" + } + + return phone + } + + static func getContactSecondaryPhone(for contact: CNContact) -> String { + let userPhoneNumbers: [CNLabeledValue] = contact.phoneNumbers + var secondary_phone: String - if !contactAlreadyAdded(name: name) { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return - } - - let managedObjectContext = appDelegate.persistentContainer.viewContext - let entity = NSEntityDescription.entity(forEntityName: "SelectedContact", in: managedObjectContext)! - - let selectedContact = NSManagedObject(entity: entity, insertInto: managedObjectContext) - - selectedContact.setValue(id, forKeyPath: "id") - selectedContact.setValue(address, forKeyPath: "address") - selectedContact.setValue(anniversary, forKeyPath: "anniversary") - selectedContact.setValue(anniversary_notification_ID, forKeyPath: "anniversary_notification_id") - selectedContact.setValue(birthday, forKeyPath: "birthday") - selectedContact.setValue(birthday_notification_ID, forKeyPath: "birthday_notification_id") - selectedContact.setValue(email, forKeyPath: "email") - selectedContact.setValue(name, forKeyPath: "name") - selectedContact.setValue(notification_identifier, forKeyPath: "notification_identifier") - selectedContact.setValue(notification_preference, forKeyPath: "notification_preference") - selectedContact.setValue(notification_preference_hour, forKeyPath: "notification_preference_hour") - selectedContact.setValue(notification_preference_minute, forKeyPath: "notification_preference_minute") - selectedContact.setValue(notification_preference_weekday, forKeyPath: "notification_preference_weekday") - selectedContact.setValue(notification_preference_custom_year, forKeyPath: "notification_preference_custom_year") - selectedContact.setValue(notification_preference_custom_month, forKeyPath: "notification_preference_custom_month") - selectedContact.setValue(notification_preference_custom_day, forKeyPath: "notification_preference_custom_day") - selectedContact.setValue(phone, forKeyPath: "phone") - selectedContact.setValue(picture, forKeyPath: "picture") - selectedContact.setValue(secondary_address, forKeyPath: "secondary_address") - selectedContact.setValue(secondary_email, forKeyPath: "secondary_email") - selectedContact.setValue(secondary_phone, forKeyPath: "secondary_phone") - - do { - try managedObjectContext.save() - } catch let error as NSError { - print("Could not save. \(error), \(error.userInfo)") - } - } else { - print("Do nothing. Contact was already added to the database") - } + if userPhoneNumbers.count > 1 { + let secondPhoneNumber = userPhoneNumbers[1].value + secondary_phone = secondPhoneNumber.stringValue + } else { + secondary_phone = "" } + return secondary_phone } - - func contactAlreadyAdded(name: String) -> Bool { - print("Checking \(name)") - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return true + + static func getContactPrimaryEmail(for contact: CNContact) -> String { + let emailAddresses = contact.emailAddresses + var email: String + + if emailAddresses.count > 0 { + let firstEmail = emailAddresses[0].value + email = firstEmail as String + } else { + email = "" } - let managedObjectContext = appDelegate.persistentContainer.viewContext - let fetchRequest = NSFetchRequest(entityName: "SelectedContact") - fetchRequest.predicate = NSPredicate(format: "name = %@", name) + return email + } - var contactsWithThatName = 0 + static func getContactSecondaryEmail(for contact: CNContact) -> String { + let emailAddresses = contact.emailAddresses + var secondary_email: String + + if emailAddresses.count > 1 { + let secondEmail = emailAddresses[1].value + secondary_email = secondEmail as String + } else { + secondary_email = "" + } + + return secondary_email + } - do { - contactsWithThatName = try managedObjectContext.count(for: fetchRequest) + static func getContactPrimaryAddress(for contact: CNContact) -> String { + //contact postal address array + let addresses = contact.postalAddresses + var address: String + + //check for postal addresses and set values + if addresses.count > 0 { + let firstAddress = addresses[0].value + let fullAddress = firstAddress.street + ", " + firstAddress.city + ", " + firstAddress.state + " " + firstAddress.postalCode + address = fullAddress.replacingOccurrences(of: "\n", with: " ") + } else { + address = "" } - catch { - print("error executing fetch request: \(error)") + + return address + } + + static func getContactSecondaryAddress(for contact: CNContact) -> String { + //contact postal address array + let addresses = contact.postalAddresses + var secondary_address: String + + //check for postal addresses and set values + if addresses.count > 1 { + let secondAddress = addresses[1].value + let fullAddress = secondAddress.street + ", " + secondAddress.city + ", " + secondAddress.state + " " + secondAddress.postalCode + secondary_address = fullAddress.replacingOccurrences(of: "\n", with: " ") + } else { + secondary_address = "" } + + return secondary_address + } - print("contacts with that name: \(contactsWithThatName)") - return contactsWithThatName > 0 + static func getContactBirthday(for contact: CNContact) -> String { + var birthdayString: String + + if contact.birthday != nil { + + let birthday = contact.birthday?.date + + let formatter = DateFormatter() + formatter.dateFormat = "MM-dd" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + + birthdayString = formatter.string(from: birthday!) + + let birthdayDate = formatter.date(from: birthdayString)! + birthdayString = formatter.string(from: birthdayDate) + + } else { + birthdayString = "" + } + + return birthdayString } + + static func getContactAnniversary(for contact: CNContact) -> String { + //check for anniversary and set value for anniversary and reminder preference + var anniversaryString: String + + let anniversary = contact.dates.filter { date -> Bool in + + guard let label = date.label else { + return false + } + + return label.contains("Anniversary") + + } .first?.value as DateComponents? + + if anniversary?.date != nil { + + let formatter = DateFormatter() + formatter.dateFormat = "MM-dd" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + + anniversaryString = formatter.string(from: (anniversary?.date!)!) + + let anniversaryDate = formatter.date(from: anniversaryString)! + anniversaryString = formatter.string(from: anniversaryDate) + + } else { + anniversaryString = "" + } + + return anniversaryString + } + + static func getFirstName(for contact: SelectedContact) -> String { + contact.name.components(separatedBy: " ").first ?? contact.name + } + + static func getFriendlyNextCatchUpTime(for contact: SelectedContact) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + + if let date = dateFormatter.date(from: contact.next_notification_date_time) { + let friendlyFormatter = DateFormatter() + + if Calendar.current.isDateInToday(date) { + friendlyFormatter.dateFormat = "h:mm a" + return "Today at \(friendlyFormatter.string(from: date))" + } else if Calendar.current.isDateInTomorrow(date) { + friendlyFormatter.dateFormat = "h:mm a" + return "Tomorrow at \(friendlyFormatter.string(from: date))" + } else { + friendlyFormatter.dateFormat = "MMMM d 'at' h:mm a" + return friendlyFormatter.string(from: date) + } + } else { + return "None" + } + } + + static func createSelectedContact(contact: CNContact) -> SelectedContact { + let currentMinute = Calendar.current.component(.minute, from: Date()) + let currentHour = Calendar.current.component(.hour, from: Date()) + let currentDay = Calendar.current.component(.day, from: Date()) + let currentMonth = Calendar.current.component(.month, from: Date()) + let currentYear = Calendar.current.component(.year, from: Date()) + let currentWeekOfMonth = Calendar.current.component(.weekOfMonth, from: Date()) + + let id = UUID() + let address = ContactHelper.getContactPrimaryAddress(for: contact) + let anniversary = ContactHelper.getContactAnniversary(for: contact) + let anniversary_notification_ID = UUID() + let birthday = ContactHelper.getContactBirthday(for: contact) + let birthday_notification_ID = UUID() + let email = ContactHelper.getContactPrimaryEmail(for: contact) + let name = ContactHelper.getContactName(for: contact) + let notification_identifier = UUID() + let notification_preference = 0 + let notification_preference_hour = currentHour + let notification_preference_minute = currentMinute + let notification_preference_weekday = 0 + let notification_preference_custom_year = currentYear + let notification_preference_custom_month = currentMonth + let notification_preference_custom_day = currentDay + let phone = ContactHelper.getContactPrimaryPhone(for: contact) + let picture = ContactHelper.encodeContactPicture(for: contact) + let secondary_email = ContactHelper.getContactSecondaryEmail(for: contact) + let secondary_address = ContactHelper.getContactSecondaryAddress(for: contact) + let secondary_phone = ContactHelper.getContactSecondaryPhone(for: contact) + + let selectedContact = SelectedContact( + address: address, + anniversary: anniversary, + anniversary_notification_id: anniversary_notification_ID, + birthday: birthday, + birthday_notification_id: birthday_notification_ID, + email: email, + id: id, + name: name, + next_notification_date_time: "", + notification_identifier: notification_identifier, + notification_preference: notification_preference, + notification_preference_custom_day: notification_preference_custom_day, + notification_preference_custom_month: notification_preference_custom_month, + notification_preference_custom_year: notification_preference_custom_year, + notification_preference_hour: notification_preference_hour, + notification_preference_minute: notification_preference_minute, + notification_preference_weekday: notification_preference_weekday, + notification_preference_week_of_month: currentWeekOfMonth, + phone: phone, + picture: picture, + secondary_address: secondary_address, + secondary_email: secondary_email, + secondary_phone: secondary_phone, + unread_badge_date_time: "" + ) + + return selectedContact + } + + static func updateSelectedContacts(_ selectedContacts: [SelectedContact]) { + for contact in selectedContacts { + updateSelectedContact(contact) + } + } + + static func updateSelectedContact(_ selectedContact: SelectedContact?) { + guard let selectedContact else { return } + + getCNContactByName(selectedContact.name) { contact in + if let contact { + let nextNotificationDateTime = NotificationHelper.getNextNotificationDateFor(contact: selectedContact) + + selectedContact.name = getContactName(for: contact) + selectedContact.phone = getContactPrimaryPhone(for: contact) + selectedContact.secondary_phone = getContactSecondaryPhone(for: contact) + selectedContact.email = getContactPrimaryEmail(for: contact) + selectedContact.secondary_email = getContactSecondaryEmail(for: contact) + selectedContact.address = getContactPrimaryAddress(for: contact) + selectedContact.secondary_address = getContactSecondaryAddress(for: contact) + selectedContact.picture = encodeContactPicture(for: contact) + selectedContact.birthday = getContactBirthday(for: contact) + selectedContact.anniversary = getContactAnniversary(for: contact) + selectedContact.next_notification_date_time = nextNotificationDateTime + } else { + print("No contact with name \(selectedContact.name) found") + } + } + } + + static func getCNContactByName(_ name: String, completion: @escaping (CNContact?) -> Void) { + print("searching contact book for \(name)") + + let contactStore = CNContactStore() + let keysToFetch: [CNKeyDescriptor] = [ + CNContactFormatter.descriptorForRequiredKeys(for: .fullName), + CNContactGivenNameKey as CNKeyDescriptor, + CNContactFamilyNameKey as CNKeyDescriptor, + CNContactPhoneNumbersKey as CNKeyDescriptor, + CNContactEmailAddressesKey as CNKeyDescriptor, + CNContactPostalAddressesKey as CNKeyDescriptor, + CNContactImageDataAvailableKey as CNKeyDescriptor, + CNContactImageDataKey as CNKeyDescriptor, + CNContactThumbnailImageDataKey as CNKeyDescriptor, + CNContactBirthdayKey as CNKeyDescriptor, + CNContactDatesKey as CNKeyDescriptor + ] + + DispatchQueue.global(qos: .background).async { + var allContacts: [CNContact] = [] + + do { + try contactStore.enumerateContacts(with: CNContactFetchRequest(keysToFetch: keysToFetch)) { (contact, stop) in + allContacts.append(contact) + } + } catch { + print("Unable to fetch contacts") + DispatchQueue.main.async { + completion(nil) + } + return + } + + let nameFormatter = CNContactFormatter() + nameFormatter.style = .fullName + + let filteredContacts = allContacts.filter { contact in + return nameFormatter.string(from: contact) == name + } + + print("Found matching contact: \(filteredContacts.first?.givenName ?? "Unknown")") + + DispatchQueue.main.async { + completion(filteredContacts.first) + } + } + } } diff --git a/CatchUp-SwiftUI/Utilities/Conversions.swift b/CatchUp-SwiftUI/Utilities/Conversions.swift index 33d9383..d0b56f3 100644 --- a/CatchUp-SwiftUI/Utilities/Conversions.swift +++ b/CatchUp-SwiftUI/Utilities/Conversions.swift @@ -10,24 +10,27 @@ import Foundation import SwiftUI import PhoneNumberKit -struct Conversions { - +struct Converter { // MARK: Only used in DetailScreen - - func getFormattedPhoneNumber(from phoneNumber: String) -> String { + static func getFormattedPhoneNumber(from phoneNumber: String) -> String { let phoneNumberKit = PhoneNumberKit() + + print("formatting phone number: \(phoneNumber)") + do { - let parsedPhoneNumber = try phoneNumberKit.parse(phoneNumber) + let parsedPhoneNumber = try phoneNumberKit.parse(phoneNumber.trimmingCharacters(in: .whitespacesAndNewlines)) let formattedPhoneNumber = phoneNumberKit.format(parsedPhoneNumber, toType: .national) return formattedPhoneNumber } catch { - print("PhoneNumberKit Parse Error") + print("PhoneNumberKit Parse Error: \(error)") return phoneNumber } } - func getTappablePhoneNumber(from phoneNumber: String) -> URL { + static func getTappablePhoneNumber(from phoneNumber: String) -> URL { + print("getting tappable phone number: \(phoneNumber)") + let tel = "tel://" let cleanNumber = phoneNumber.replacingOccurrences(of: "[^\\d+]", with: "", options: [.regularExpression]) let formattedString = tel + cleanNumber @@ -36,7 +39,7 @@ struct Conversions { return tappableNumber } - func getTappableEmail(from emailAddress: String) -> URL { + static func getTappableEmail(from emailAddress: String) -> URL { let mailto = "mailto:" let formattedString = mailto + emailAddress let tappableEmail = URL(string: formattedString)! @@ -44,7 +47,7 @@ struct Conversions { return tappableEmail } - func getFormattedBirthdayOrAnniversary(from storedDate: String) -> String { + static func getFormattedBirthdayOrAnniversary(from storedDate: String) -> String { var month = storedDate.prefix(2) let day = storedDate.suffix(2) @@ -96,7 +99,7 @@ struct Conversions { // MARK: Used in HomeScreen and DetailScreen - func getContactPicture(from string: String) -> Image { + static func getContactPicture(from string: String) -> Image { let imageData = NSData(base64Encoded: string) let uiImage = UIImage(data: imageData! as Data)! let image = Image(uiImage: uiImage) @@ -104,7 +107,7 @@ struct Conversions { return image } - func convertNotificationPreferenceIntToString(preference: Int, contact: SelectedContact) -> String { + static func convertNotificationPreferenceIntToString(preference: Int, contact: SelectedContact) -> String { let time = convertHourAndMinuteFromIntToString(for: contact) let weekday = convertWeekdayFromIntToString(for: contact) let customDate = convertCustomDateFromIntToString(for: contact) @@ -125,7 +128,7 @@ struct Conversions { } } - func convertWeekdayFromIntToString(for contact: SelectedContact) -> String { + static func convertWeekdayFromIntToString(for contact: SelectedContact) -> String { let weekday: String switch contact.notification_preference_weekday { @@ -158,7 +161,7 @@ struct Conversions { return weekday } - func convertHourAndMinuteFromIntToString(for contact: SelectedContact) -> String { + static func convertHourAndMinuteFromIntToString(for contact: SelectedContact) -> String { var hour: String var suffix: String @@ -262,7 +265,7 @@ struct Conversions { return time } - func convertCustomDateFromIntToString(for contact: SelectedContact) -> String { + static func convertCustomDateFromIntToString(for contact: SelectedContact) -> String { var month: String var day: String var year: String diff --git a/CatchUp-SwiftUI/Utilities/GeneralHelpers.swift b/CatchUp-SwiftUI/Utilities/GeneralHelpers.swift deleted file mode 100644 index 4a0b46c..0000000 --- a/CatchUp-SwiftUI/Utilities/GeneralHelpers.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// GeneralHelpers.swift -// CatchUp-SwiftUI -// -// Created by Ryan Token on 4/28/20. -// Copyright © 2020 Token Solutions. All rights reserved. -// - -import SwiftUI -import Foundation -import CoreData - -struct GeneralHelpers { - - func clearNotificationBadge() { - UIApplication.shared.applicationIconBadgeNumber = 0 - } - - func saveMOC(moc: NSManagedObjectContext) { - do { - try moc.save() - } catch let error as NSError { - print("Could not update the MOC. \(error), \(error.userInfo)") - } - } - - func getCurrentAppVersion() -> String { - let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] - let version = (appVersion as! String) - - print(version) - return version - } - -} diff --git a/CatchUp-SwiftUI/Utilities/NotificationHelper.swift b/CatchUp-SwiftUI/Utilities/NotificationHelper.swift new file mode 100644 index 0000000..8a29083 --- /dev/null +++ b/CatchUp-SwiftUI/Utilities/NotificationHelper.swift @@ -0,0 +1,375 @@ +// +// NotificationHelper.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 4/21/20. +// Copyright © 2020 Token Solutions. All rights reserved. +// + +import SwiftData +import SwiftUI +import UserNotifications +import CoreData + +struct NotificationHelper { + @MainActor + static func createNewNotification(for contact: SelectedContact) { + updateNextNotificationDateTimeFor(contact: contact) + + let addRequest = { + if preferenceIsNotSetToNever(for: contact) { + addGeneralNotification(for: contact) + } + + if ContactHelper.contactHasBirthday(contact) { + addBirthdayNotification(for: contact) + } + + if ContactHelper.contactHasAnniversary(contact) { + addAnniversaryNotification(for: contact) + } + } + + checkNotificationAuthorizationStatusAndAddRequest(action: addRequest) + } + + static func preferenceIsNotSetToNever(for contact: SelectedContact) -> Bool { + return contact.notification_preference != 0 ? true : false + } + + static func addGeneralNotification(for contact: SelectedContact) { + let notificationContent = UNMutableNotificationContent() + notificationContent.title = "👋 CatchUp with \(contact.name)" + notificationContent.body = generateRandomNotificationBody() + notificationContent.sound = UNNotificationSound.default + notificationContent.badge = 1 + + let identifier = UUID() + let dateComponents = getNotificationDateComponents(for: contact) + + scheduleNotification(for: contact, dateComponents: dateComponents, identifier: identifier, content: notificationContent) + } + + static func addBirthdayNotification(for contact: SelectedContact) { + let birthdayNotificationContent = UNMutableNotificationContent() + birthdayNotificationContent.title = "🥳 Today is \(contact.name)'s birthday!" + birthdayNotificationContent.body = "Be sure to CatchUp and wish them a great one!" + birthdayNotificationContent.sound = UNNotificationSound.default + birthdayNotificationContent.badge = 1 + + let birthdayIdentifier = UUID() + let birthdayDateComponents = getBirthdayDateComponents(for: contact) + + scheduleNotification(for: contact, dateComponents: birthdayDateComponents, identifier: birthdayIdentifier, content: birthdayNotificationContent) + } + + static func addAnniversaryNotification(for contact: SelectedContact) { + let anniversaryNotificationContent = UNMutableNotificationContent() + anniversaryNotificationContent.title = "😍 Tomorrow is \(contact.name)'s anniversary!" + anniversaryNotificationContent.body = "Be sure to CatchUp and wish them the best." + anniversaryNotificationContent.sound = UNNotificationSound.default + anniversaryNotificationContent.badge = 1 + + let anniversaryIdentifier = UUID() + let anniversaryDateComponents = getAnniversaryDateComponents(for: contact) + + scheduleNotification(for: contact, dateComponents: anniversaryDateComponents, identifier: anniversaryIdentifier, content: anniversaryNotificationContent) + } + + static func checkNotificationAuthorizationStatusAndAddRequest(action: @escaping() -> Void) { + UNUserNotificationCenter.current().getNotificationSettings { settings in + if settings.authorizationStatus == .authorized { + action() + } else { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in + if success { + print("Scheduled new notification(s)") + action() + } else { + print("User isn't allowing notifications :(") + } + } + } + } + } + + static func getNotificationDateComponents(for contact: SelectedContact) -> DateComponents { + var dateComponents = DateComponents() + + switch contact.notification_preference { + case 0: // Never + break + case 1: // Daily + dateComponents.hour = contact.notification_preference_hour + dateComponents.minute = contact.notification_preference_minute + break + case 2: // Weekly + dateComponents.hour = contact.notification_preference_hour + dateComponents.minute = contact.notification_preference_minute + // weekday units are 1-7, I store them as 0-6 though. Need to add 1 + dateComponents.weekday = contact.notification_preference_weekday+1 + break + case 3: // Monthly + dateComponents.hour = contact.notification_preference_hour + dateComponents.minute = contact.notification_preference_minute + dateComponents.weekday = contact.notification_preference_weekday+1 + dateComponents.weekOfMonth = contact.notification_preference_week_of_month + break + case 4: // Custom Date + dateComponents.month = contact.notification_preference_custom_month + dateComponents.day = contact.notification_preference_custom_day + dateComponents.year = contact.notification_preference_custom_year + dateComponents.hour = contact.notification_preference_hour + dateComponents.minute = contact.notification_preference_minute + break + default: + print("It's impossible to get here") + } + + return dateComponents + } + + static func getBirthdayDateComponents(for contact: SelectedContact) -> DateComponents { + var birthdayDateComponents = DateComponents() + + let month = (contact.birthday).prefix(2) + let day = (contact.birthday).suffix(2) + + birthdayDateComponents.month = Int(month) + birthdayDateComponents.day = Int(day) + birthdayDateComponents.hour = 7 + birthdayDateComponents.minute = 15 + + return birthdayDateComponents + } + + static func getAnniversaryDateComponents(for contact: SelectedContact) -> DateComponents { + var anniversaryDateComponents = DateComponents() + let formatter = DateFormatter() + + formatter.dateFormat = "MM-dd" + let anniversaryDate = formatter.date(from: contact.anniversary)! + let previousDayDate = Calendar.current.date(byAdding: .day, value: -1, to: anniversaryDate) + let previousDay = formatter.string(from: previousDayDate!) + + let month = (previousDay).prefix(2) + let day = (previousDay).suffix(2) + + anniversaryDateComponents.month = Int(month) + anniversaryDateComponents.day = Int(day) + anniversaryDateComponents.hour = 7 + anniversaryDateComponents.minute = 30 + + return anniversaryDateComponents + } + + static func scheduleNotification(for contact: SelectedContact, dateComponents: DateComponents, identifier: UUID, content: UNMutableNotificationContent) { + + let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true) + let request = UNNotificationRequest(identifier: identifier.uuidString, content: content, trigger: trigger) + + if content.title == "👋 CatchUp with \(contact.name)" { + contact.notification_identifier = identifier + } else if content.title == "🥳 Today is \(contact.name)'s birthday!" { + contact.birthday_notification_id = identifier + } else { + contact.anniversary_notification_id = identifier + } + + print("scheduling notification for \(contact.name) with date components: \(trigger.dateComponents)") + + UNUserNotificationCenter.current().add(request) + } + + static func generateRandomNotificationBody() -> String { + let randomInt = Int.random(in: 0..<20) + + switch randomInt { + case 0: + return "Now you can be best buddies again" + case 1: + return "A little birdy told me they really miss you" + case 2: + return "It's time to check back in" + case 3: + return "You're a good friend. Good for you. Tell this person to get CatchUp too so it's not always you who's reaching out" + case 4: + return "Today is the perfect day to get back in touch" + case 5: + return "Remember to keep in touch with the people that matter most" + case 6: + return "You know what they say: 'A CatchUp a day keeps the needy friends at bay'" + case 7: + return "Have you written a physical letter in a while? Maybe give that a try this time. People like that" + case 8: + return "Here's that reminder you set to check in with someone important. Maybe you'll make their day" + case 9: + return "Once a good person, always a good person (you are a good person, and probably so is the person you want to be reminded to CatchUp with)" + case 10: + return "Here's another reminder to get back in touch with ⬆️" + case 11: + return "Time to get back in contact with one of your favorite people" + case 12: + return "So nice of you to want to stay in touch with the people you care about" + case 13: + return "You know they'll really appreciate it" + case 14: + return "I'm not guilting you into this or anything, but this person will probably be sad if you don't say hello." + case 15: + return "You have this app, so you must be cool and nice. Now send a thoughtful message to this also cool and nice person" + case 16: + return "Once upon a time, there was a nice person. The end. (Spoiler: you're the nice person - keep being nice and reach out to your friend)" + case 17: + return "Another timely reminder to catch up. Just the way you wanted it" + case 18: + return "Now is the time! Seize the moment!" + case 19: + return "Good job keeping your friends close. Now keep your enemies closer 😉" + default: + return "Keep in touch" + } + } + + static func updateNotificationPreference(for contact: SelectedContact, selection: Int) { + contact.notification_preference = selection + } + + static func updateNotificationTime(for contact: SelectedContact, hour: Int, minute: Int) { + contact.notification_preference_hour = hour + contact.notification_preference_minute = minute + } + + static func updateNotificationPreferenceWeekday(for contact: SelectedContact, weekday: Int) { + contact.notification_preference_weekday = weekday + } + + static func updateNotificationCustomDate(for contact: SelectedContact, month: Int, day: Int, year: Int) { + contact.notification_preference_custom_month = month + contact.notification_preference_custom_day = day + contact.notification_preference_custom_year = year + } + + static func requestAuthorizationForNotifications() { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in + if success { + print("User authorized CatchUp to send notifications") + } else if let error = error { + print(error.localizedDescription) + } + } + } + + static func removeExistingNotifications(for contact: SelectedContact) { + removeGeneralNotification(for: contact) + + if ContactHelper.contactHasBirthday(contact) { + removeBirthdayNotification(for: contact) + } + + if ContactHelper.contactHasAnniversary(contact) { + removeAnniversaryNotification(for: contact) + } + } + + static func removeGeneralNotification(for contact: SelectedContact) { + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [contact.notification_identifier.uuidString]) + + UNUserNotificationCenter.current().getPendingNotificationRequests { requests in + print("Pending requests after removing existing request: \(requests.count)") + } + } + + static func removeBirthdayNotification(for contact: SelectedContact) { + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [contact.birthday_notification_id.uuidString]) + } + + static func removeAnniversaryNotification(for contact: SelectedContact) { + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [contact.anniversary_notification_id.uuidString]) + } + + @MainActor + static func updateNextNotificationDateTimeFor(contact: SelectedContact) { + let nextNotificationDateTime = getNextNotificationDateFor(contact: contact) + contact.next_notification_date_time = nextNotificationDateTime + } + + static func getNextNotificationDateFor(contact: SelectedContact) -> String { + // Get next notification date for the general notification + var components = DateComponents() + switch contact.notification_preference { + case 1: // daily + components.hour = contact.notification_preference_hour + components.minute = contact.notification_preference_minute + case 2, 3: // weekly, monthly + components.hour = contact.notification_preference_hour + components.minute = contact.notification_preference_minute + components.weekday = contact.notification_preference_weekday + 1 + if contact.notification_preference_week_of_month != 0 { + components.weekOfMonth = contact.notification_preference_week_of_month + } + case 4: // custom date + components.hour = contact.notification_preference_hour + components.minute = contact.notification_preference_minute + components.month = contact.notification_preference_custom_month + components.day = contact.notification_preference_custom_day + components.year = contact.notification_preference_custom_year + default: + print("do nothing") + } + + var soonestUpcomingNotificationDateString = "Unknown" + soonestUpcomingNotificationDateString = calculateDateFromComponents(components) + + if ContactHelper.contactHasBirthday(contact) { + let birthdayDateString = calculateDateFromComponents(getBirthdayDateComponents(for: contact)) + if birthdayDateString < soonestUpcomingNotificationDateString { + soonestUpcomingNotificationDateString = birthdayDateString + } + } + + if ContactHelper.contactHasAnniversary(contact) { + let anniversaryDateString = calculateDateFromComponents(getAnniversaryDateComponents(for: contact)) + if anniversaryDateString < soonestUpcomingNotificationDateString { + soonestUpcomingNotificationDateString = anniversaryDateString + } + } + + return soonestUpcomingNotificationDateString + } + + static func calculateDateFromComponents(_ dateComponents: DateComponents) -> String { + let calendar = Calendar.current + let currentDate = Date() + + // Calculate the date based on the provided components and current date + if let calculatedDate = calendar.nextDate(after: currentDate, matching: dateComponents, matchingPolicy: .nextTime) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" // Define the desired date format + + // Convert the calculated date to a human-readable date string + let formattedDate = dateFormatter.string(from: calculatedDate) + return formattedDate + } + + return "Unknown" + } + + @MainActor + static func resetNotifications(for selectedContacts: [SelectedContact], delayTime: Double) { + DispatchQueue.main.asyncAfter(deadline: .now() + delayTime) { + print("resetting notifications") + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + + for contact in selectedContacts { + if contact.notification_preference != 0 { + NotificationHelper.createNewNotification(for: contact) + } + + if contact.unread_badge_date_time == "" { + print("updating unread badge date time for \(contact.name)") + contact.unread_badge_date_time = contact.next_notification_date_time + } + } + } + } +} diff --git a/CatchUp-SwiftUI/Utilities/Utils.swift b/CatchUp-SwiftUI/Utilities/Utils.swift new file mode 100644 index 0000000..92c4fe4 --- /dev/null +++ b/CatchUp-SwiftUI/Utilities/Utils.swift @@ -0,0 +1,54 @@ +// +// Utils.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 4/28/20. +// Copyright © 2020 Token Solutions. All rights reserved. +// + +import SwiftUI +import Foundation +import CoreData +import UserNotifications + +struct Utils { + static func clearAppIconNotificationBadge() { + UNUserNotificationCenter.current().setBadgeCount(0) + } + + static func clearUnreadBadge(for contact: SelectedContact) { + contact.unread_badge_date_time = contact.next_notification_date_time + } + + static func getCurrentAppVersion() -> String { + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] + let version = (appVersion as! String) + + print(version) + return version + } + + static func updateIsMajor() -> Bool { + let version = getCurrentAppVersion() + if version.suffix(2) == ".0" { + return true + } else { + return false + } + } + + static func fetchAvailableIAPs() { + print("fetching IAPs") + IAPService.shared.fetchAvailableProducts() + } + + @MainActor + static func isPhone() -> Bool { + return UIDevice.current.userInterfaceIdiom == .phone + } + + @MainActor + static func isiPadOrMac() -> Bool { + return UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac + } +} diff --git a/CatchUp-SwiftUI/AboutScreen.swift b/CatchUp-SwiftUI/Views/AboutScreen.swift similarity index 57% rename from CatchUp-SwiftUI/AboutScreen.swift rename to CatchUp-SwiftUI/Views/AboutScreen.swift index e06756c..8c0be96 100644 --- a/CatchUp-SwiftUI/AboutScreen.swift +++ b/CatchUp-SwiftUI/Views/AboutScreen.swift @@ -9,10 +9,8 @@ import SwiftUI struct AboutScreen: View { - @State private var showingUpdateScreen = false - - let generator = UINotificationFeedbackGenerator() - + @State private var isShowingUpdateScreen = false + let smallTip = IAPService.shared.getSmallTipAmount() let mediumTip = IAPService.shared.getMediumTipAmount() let largeTip = IAPService.shared.getLargeTipAmount() @@ -30,7 +28,7 @@ struct AboutScreen: View { .shadow(radius: 10) Text("CatchUp") - .foregroundColor(.orange) + .foregroundStyle(.orange) .font(.largeTitle) .bold() @@ -58,38 +56,38 @@ struct AboutScreen: View { Spacer() Button(smallTip) { - self.graciousTipPressed() + tappedSmallTip() } - .font(.headline) - .foregroundColor(.white) - .padding() - .background(RoundedRectangle(cornerRadius: 20).fill(LinearGradient(gradient: Gradient(colors: [.orange, .red]), startPoint: .top, endPoint: .bottom)) - ) - .shadow(radius: 15) + .font(.headline) + .foregroundStyle(.white) + .padding() + .background(RoundedRectangle(cornerRadius: 20).fill(LinearGradient(gradient: Gradient(colors: [.orange, .red]), startPoint: .top, endPoint: .bottom)) + ) + .shadow(radius: 15) Spacer() Button(mediumTip) { - self.generousTipPressed() + tappedMediumTip() } - .font(.headline) - .foregroundColor(.white) - .padding() - .background(RoundedRectangle(cornerRadius: 20).fill(LinearGradient(gradient: Gradient(colors: [.orange, .red]), startPoint: .top, endPoint: .bottom)) - ) - .shadow(radius: 15) + .font(.headline) + .foregroundStyle(.white) + .padding() + .background(RoundedRectangle(cornerRadius: 20).fill(LinearGradient(gradient: Gradient(colors: [.orange, .red]), startPoint: .top, endPoint: .bottom)) + ) + .shadow(radius: 15) Spacer() Button(largeTip) { - self.gratuitousTipPressed() + tappedLargeTip() } - .font(.headline) - .foregroundColor(.white) - .padding() - .background(RoundedRectangle(cornerRadius: 20).fill(LinearGradient(gradient: Gradient(colors: [.orange, .red]), startPoint: .top, endPoint: .bottom)) - ) - .shadow(radius: 15) + .font(.headline) + .foregroundStyle(.white) + .padding() + .background(RoundedRectangle(cornerRadius: 20).fill(LinearGradient(gradient: Gradient(colors: [.orange, .red]), startPoint: .top, endPoint: .bottom)) + ) + .shadow(radius: 15) Spacer() } @@ -105,40 +103,41 @@ struct AboutScreen: View { } Group { - Button(action: { - self.showingUpdateScreen = true - }) { + Button { + isShowingUpdateScreen = true + } label: { Text("Show Latest Update Details") .font(.headline) - .foregroundColor(.blue) + .foregroundStyle(.blue) } } } .padding() - .sheet(isPresented: $showingUpdateScreen) { + .sheet(isPresented: $isShowingUpdateScreen) { UpdatesScreen() } } - - func graciousTipPressed() { - generator.notificationOccurred(.success) - IAPService.shared.leaveATip(index: 1) + + @MainActor + func tappedSmallTip() { + UINotificationFeedbackGenerator().notificationOccurred(.success) + IAPService.shared.leaveATip(index: 0) } - - func generousTipPressed() { - generator.notificationOccurred(.success) - IAPService.shared.leaveATip(index: 0) + + @MainActor + func tappedMediumTip() { + UINotificationFeedbackGenerator().notificationOccurred(.success) + IAPService.shared.leaveATip(index: 1) } - func gratuitousTipPressed() { - generator.notificationOccurred(.success) + @MainActor + func tappedLargeTip() { + UINotificationFeedbackGenerator().notificationOccurred(.success) IAPService.shared.leaveATip(index: 2) } } -struct AboutScreen_Previews: PreviewProvider { - static var previews: some View { - AboutScreen() - } +#Preview { + AboutScreen() } diff --git a/CatchUp-SwiftUI/Views/DetailScreen Subviews/BirthdayOrAnniversaryRow.swift b/CatchUp-SwiftUI/Views/DetailScreen Subviews/BirthdayOrAnniversaryRow.swift new file mode 100644 index 0000000..934e630 --- /dev/null +++ b/CatchUp-SwiftUI/Views/DetailScreen Subviews/BirthdayOrAnniversaryRow.swift @@ -0,0 +1,53 @@ +// +// BirthdayOrAnniversaryRow.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/31/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import SwiftUI + +struct BirthdayOrAnniversaryRow: View { + let contact: SelectedContact + + var body: some View { + if contact.next_notification_date_time.contains(contact.birthday) { + VStack { + HStack { + Spacer() + Text("🥳 \(ContactHelper.getFirstName(for: contact))'s birthday!") + .foregroundStyle(.orange) + Spacer() + } + .padding(.top, 2) + + Spacer() + } + } else if contact.next_notification_date_time == dayBeforeAnniversaryString() { + VStack { + HStack { + Spacer() + Text("🧡 The day before your anniversary!") + .foregroundStyle(.orange) + Spacer() + } + .padding(.top, 2) + + Spacer() + } + } + } + + func dayBeforeAnniversaryString() -> String? { + if ContactHelper.contactHasAnniversary(contact) { + return NotificationHelper.calculateDateFromComponents(NotificationHelper.getAnniversaryDateComponents(for: contact)) + } + + return nil + } +} + +#Preview { + BirthdayOrAnniversaryRow(contact: SelectedContact.sampleData) +} diff --git a/CatchUp-SwiftUI/Views/DetailScreen Subviews/ContactInfoView.swift b/CatchUp-SwiftUI/Views/DetailScreen Subviews/ContactInfoView.swift new file mode 100644 index 0000000..90247a4 --- /dev/null +++ b/CatchUp-SwiftUI/Views/DetailScreen Subviews/ContactInfoView.swift @@ -0,0 +1,161 @@ +// +// ContactInfoView.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/10/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import MapKit +import SwiftUI + +struct ContactInfoView: View { + @State private var isShowingEmailAlert = false + @State private var emailString = "" + @State private var emailUrlForAlert: URL? + + let contact: SelectedContact + + var formattedPrimaryPhoneNumber: String { + Converter.getFormattedPhoneNumber(from: contact.phone) + } + + var formattedSecondaryPhoneNumber: String { + Converter.getFormattedPhoneNumber(from: contact.secondary_phone) + } + + var tappablePrimaryPhoneNumber: URL { + Converter.getTappablePhoneNumber(from: contact.phone) + } + + var tappableSecondaryPhoneNumber: URL { + Converter.getTappablePhoneNumber(from: contact.secondary_phone) + } + + var tappablePrimaryEmail: URL { + Converter.getTappableEmail(from: contact.email) + } + + var tappableSecondaryEmail: URL { + Converter.getTappableEmail(from: contact.secondary_email) + } + + var body: some View { + if ContactHelper.contactHasPhone(contact) { + VStack(alignment: .leading, spacing: 3) { + Text("Phone") + .font(.caption) + + Button(formattedPrimaryPhoneNumber) { + UIApplication.shared.open(tappablePrimaryPhoneNumber) + } + .foregroundStyle(.blue) + } + } + if ContactHelper.contactHasSecondaryPhone(contact) { + VStack(alignment: .leading, spacing: 3) { + Text("Secondary Phone") + .font(.caption) + + Button(formattedSecondaryPhoneNumber) { + UIApplication.shared.open(tappableSecondaryPhoneNumber) + } + .foregroundStyle(.blue) + } + } + if ContactHelper.contactHasEmail(contact) { + VStack(alignment: .leading, spacing: 3) { + Text("Email") + .font(.caption) + + Button(contact.email) { + emailString = contact.email + emailUrlForAlert = tappablePrimaryEmail + isShowingEmailAlert = true + } + .foregroundStyle(.blue) + } + + .alert("Email \(emailString)?", isPresented: $isShowingEmailAlert) { + if let emailUrlForAlert { + Button("Yes") { + UIApplication.shared.open(emailUrlForAlert) + } + } + + Button("Cancel", role: .cancel) {} + } + } + if ContactHelper.contactHasSecondaryEmail(contact) { + VStack(alignment: .leading, spacing: 3) { + Text("Secondary Email") + .font(.caption) + + Button(contact.secondary_email) { + emailString = contact.secondary_email + emailUrlForAlert = tappableSecondaryEmail + isShowingEmailAlert = true + } + .foregroundStyle(.blue) + } + } + if ContactHelper.contactHasAddress(contact) { + VStack(alignment: .leading, spacing: 3) { + Text("Address") + .font(.caption) + Button(contact.address) { + openAddressInMaps(address: contact.address) + } + .foregroundStyle(.blue) + } + } + if ContactHelper.contactHasSecondaryAddress(contact) { + VStack(alignment: .leading, spacing: 3) { + Text("Secondary Address") + .font(.caption) + Button(contact.secondary_address) { + openAddressInMaps(address: contact.secondary_address) + } + .foregroundStyle(.blue) + } + } + if ContactHelper.contactHasBirthday(contact) { + VStack(alignment: .leading, spacing: 3) { + Text("Birthday") + .font(.caption) + Text(Converter.getFormattedBirthdayOrAnniversary(from: contact.birthday)) + } + } + if ContactHelper.contactHasAnniversary(contact) { + VStack(alignment: .leading, spacing: 3) { + Text("Anniversary") + .font(.caption) + Text(Converter.getFormattedBirthdayOrAnniversary(from: contact.anniversary)) + } + } + } + + func openAddressInMaps(address: String){ + let geocoder = CLGeocoder() + geocoder.geocodeAddressString(address) { (placemarks, error) in + guard let placemarks = placemarks?.first else { + return + } + + let location = placemarks.location?.coordinate + + if let lat = location?.latitude, let long = location?.longitude{ + let destination = MKMapItem(placemark: MKPlacemark(coordinate: CLLocationCoordinate2D(latitude: lat, longitude: long))) + destination.name = address + + MKMapItem.openMaps( + with: [destination] + ) + } + } + } +} + +#Preview { + ContactInfoView(contact: SelectedContact.sampleData) +} diff --git a/CatchUp-SwiftUI/Supporting Views/ContactPhoto.swift b/CatchUp-SwiftUI/Views/DetailScreen Subviews/ContactPhoto.swift similarity index 76% rename from CatchUp-SwiftUI/Supporting Views/ContactPhoto.swift rename to CatchUp-SwiftUI/Views/DetailScreen Subviews/ContactPhoto.swift index 2aea393..c8a06ff 100644 --- a/CatchUp-SwiftUI/Supporting Views/ContactPhoto.swift +++ b/CatchUp-SwiftUI/Views/DetailScreen Subviews/ContactPhoto.swift @@ -21,8 +21,6 @@ struct ContactPhoto: View { } } -struct ContactPhoto_Previews: PreviewProvider { - static var previews: some View { - ContactPhoto(image: Image("DefaultPhoto")) - } +#Preview { + ContactPhoto(image: Image("DefaultPhoto")) } diff --git a/CatchUp-SwiftUI/Supporting Views/GradientView.swift b/CatchUp-SwiftUI/Views/DetailScreen Subviews/GradientView.swift similarity index 76% rename from CatchUp-SwiftUI/Supporting Views/GradientView.swift rename to CatchUp-SwiftUI/Views/DetailScreen Subviews/GradientView.swift index db0f82f..79ac9dd 100644 --- a/CatchUp-SwiftUI/Supporting Views/GradientView.swift +++ b/CatchUp-SwiftUI/Views/DetailScreen Subviews/GradientView.swift @@ -29,15 +29,11 @@ struct GradientView: View { } } -struct Gradient_Previews: PreviewProvider { - @Environment(\.colorScheme) var colorScheme - - static var previews: some View { - VStack { - GradientView() - .edgesIgnoringSafeArea(.top) - .frame(height: 150) - Spacer() - } +#Preview { + VStack { + GradientView() + .edgesIgnoringSafeArea(.top) + .frame(height: 150) + Spacer() } } diff --git a/CatchUp-SwiftUI/Views/DetailScreen Subviews/NameAndPreferenceStack.swift b/CatchUp-SwiftUI/Views/DetailScreen Subviews/NameAndPreferenceStack.swift new file mode 100644 index 0000000..5239f79 --- /dev/null +++ b/CatchUp-SwiftUI/Views/DetailScreen Subviews/NameAndPreferenceStack.swift @@ -0,0 +1,34 @@ +// +// NameAndPreferenceStack.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/10/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import SwiftUI + +struct NameAndPreferenceStack: View { + let contact: SelectedContact + + var body: some View { + VStack(alignment: .center, spacing: 10) { + Text(contact.name) + .font(.largeTitle) + .bold() + + HStack(spacing: 0) { + Text("Preference: ") + .foregroundStyle(.gray) + + Text(Converter.convertNotificationPreferenceIntToString(preference: contact.notification_preference, contact: contact)) + .foregroundStyle(.gray) + } + } + .padding(.bottom, 5) + } +} + +#Preview { + NameAndPreferenceStack(contact: SelectedContact.sampleData) +} diff --git a/CatchUp-SwiftUI/Views/DetailScreen Subviews/NextCatchUpRow.swift b/CatchUp-SwiftUI/Views/DetailScreen Subviews/NextCatchUpRow.swift new file mode 100644 index 0000000..e1ea123 --- /dev/null +++ b/CatchUp-SwiftUI/Views/DetailScreen Subviews/NextCatchUpRow.swift @@ -0,0 +1,29 @@ +// +// NextCatchUpRow.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/29/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import SwiftUI + +struct NextCatchUpRow: View { + let nextCatchUpTime: String + + var body: some View { + HStack { + Text("Next CatchUp:") + + Spacer() + + Text(nextCatchUpTime) + .foregroundStyle(.gray) + } + .listRowSeparator(.hidden) + } +} + +#Preview { + NextCatchUpRow(nextCatchUpTime: "April 3 at 7:15 AM") +} diff --git a/CatchUp-SwiftUI/Views/DetailScreen Subviews/NotificationPreferenceView.swift b/CatchUp-SwiftUI/Views/DetailScreen Subviews/NotificationPreferenceView.swift new file mode 100644 index 0000000..c279956 --- /dev/null +++ b/CatchUp-SwiftUI/Views/DetailScreen Subviews/NotificationPreferenceView.swift @@ -0,0 +1,190 @@ +// +// NotificationPreferenceView.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/29/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import SwiftUI + +struct NotificationPreferenceView: View { + @Environment(DataController.self) var dataController + @Environment(\.modelContext) var modelContext + + @State private var initialNotificationPreference = 0 + @State private var initialNotificationPreferenceWeekday = 0 + @State private var initialNotificationPreferenceTime = Date() + @State private var initialNotificationPreferenceCustomDate = Date() + + @State private var notificationPreferenceTime = Date() + @State private var notificationPreferenceCustomDate = Date() + + @State private var whatDayText = "" + @State private var viewDidAppear = false + + @Bindable var contact: SelectedContact + + let notificationOptions = ["Never", "Daily", "Weekly", "Monthly", "Custom"] + let dayOptions = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + + var body: some View { + Group { + Picker(selection: $contact.notification_preference, label: Text("How often?")) { + ForEach(0.. Bool { + let today = Date.now + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let formattedTodayDate = formatter.string(from: today) + + if contact.unread_badge_date_time != "" && formattedTodayDate >= contact.unread_badge_date_time { + return true + } else { + return false + } + } +} + +#Preview { + ContactRowView(contact: SelectedContact.sampleData) +} diff --git a/CatchUp-SwiftUI/Views/HomeScreen Subviews/NextCatchUpsGridView.swift b/CatchUp-SwiftUI/Views/HomeScreen Subviews/NextCatchUpsGridView.swift new file mode 100644 index 0000000..cc05706 --- /dev/null +++ b/CatchUp-SwiftUI/Views/HomeScreen Subviews/NextCatchUpsGridView.swift @@ -0,0 +1,67 @@ +// +// NextCatchUpsGridView.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/24/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import SwiftUI + +struct NextCatchUpsGridView: View { + @Environment(\.colorScheme) var colorScheme + + let nextCatchUps: [SelectedContact] + @Binding var shouldNavigateViaGrid: Bool + @Binding var tappedGridContact: SelectedContact? + + // 2 column grid + let columns = [ + GridItem(.flexible(minimum: 0, maximum: .infinity)), + GridItem(.flexible(minimum: 0, maximum: .infinity)) + ] + + var body: some View { + LazyVGrid(columns: columns, spacing: 10) { + ForEach(nextCatchUps) { contact in + HStack { + ContactPictureView(contact: contact) + .padding(.trailing, 5) + + VStack(alignment: .leading, spacing: 2) { + Text(ContactHelper.getFirstName(for: contact)) + .font(.headline) + + Text(ContactHelper.getFriendlyNextCatchUpTime(for: contact)) + .foregroundStyle(.gray) + .font(.caption) + } + } + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .frame(minHeight: 55, maxHeight: 65) + .padding(10) + .background(colorScheme == .light ? Color.white : Color(UIColor(red: 0.15, green: 0.15, blue: 0.15, alpha: 1))) + .cornerRadius(10) + .if(colorScheme == .light) { view in + view.shadow(color: Color.gray.opacity(0.4), radius: 3, x: 0, y: 2) + } + + .onTapGesture { + tappedGridContact = contact + shouldNavigateViaGrid = true + } + } + } + .padding(.bottom, 5) + .listRowInsets(EdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 0)) + .listRowBackground(Color.clear) + } +} + +#Preview { + NextCatchUpsGridView( + nextCatchUps: [SelectedContact.sampleData, SelectedContact.sampleData, SelectedContact.sampleData], + shouldNavigateViaGrid: .constant(false), + tappedGridContact: .constant(SelectedContact.sampleData) + ) +} diff --git a/CatchUp-SwiftUI/Views/HomeScreen Subviews/OpenContactPickerButtonView.swift b/CatchUp-SwiftUI/Views/HomeScreen Subviews/OpenContactPickerButtonView.swift new file mode 100644 index 0000000..dcd90b9 --- /dev/null +++ b/CatchUp-SwiftUI/Views/HomeScreen Subviews/OpenContactPickerButtonView.swift @@ -0,0 +1,26 @@ +// +// OpenContactPickerButtonView.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 3/10/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import SwiftUI + +struct OpenContactPickerButtonView: View { + var body: some View { + HStack(alignment: .center, spacing: 6) { + Image(systemName: "person.crop.circle.fill.badge.plus") + + Text("Add Contacts") + } + .font(.headline) + .foregroundStyle(.blue) + .padding(.top, 10) + } +} + +#Preview { + OpenContactPickerButtonView() +} diff --git a/CatchUp-SwiftUI/Views/HomeScreen.swift b/CatchUp-SwiftUI/Views/HomeScreen.swift new file mode 100644 index 0000000..863e535 --- /dev/null +++ b/CatchUp-SwiftUI/Views/HomeScreen.swift @@ -0,0 +1,212 @@ +// +// ContentView.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 6/26/19. +// Copyright © 2019 Token Solutions. All rights reserved. +// + +import ContactsUI +import StoreKit +import SwiftData +import SwiftUI + +struct HomeScreen : View { + @Environment(\.modelContext) var modelContext + @Environment(\.scenePhase) var scenePhase + @Environment(\.requestReview) var requestReview + + @Query(sort: \SelectedContact.name) var selectedContacts: [SelectedContact] + @Query(sort: \SelectedContact.next_notification_date_time) var nextCatchups: [SelectedContact] + + @AppStorage("savedVersion") var savedVersion = "2.0.0" + @AppStorage("timesUserHasLaunchedApp") var timesUserHasLaunchedApp = 0 + + @State private var isColdLaunch = true + @State private var isShowingUpdatesSheet = false + @State private var isShowingAboutSheet = false + @State private var shouldNavigateViaGrid = false + @State private var tappedGridContact: SelectedContact? = nil + @State private var contactPicker = ContactPickerDelegate() + + var filteredNextCatchups: [SelectedContact] { + withAnimation { + return Array(nextCatchups.filter({ $0.next_notification_date_time != "" }).prefix(4)) + } + } + + @MainActor + init() { + //Use this if NavigationBarTitle is with Large Font + UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.systemOrange] + } + + var body: some View { + VStack { + List { + if selectedContacts.count > 0 { + if selectedContacts.contains(where: { $0.notification_preference != 0 }) { + Section("Next CatchUps") { + NextCatchUpsGridView(nextCatchUps: filteredNextCatchups, shouldNavigateViaGrid: $shouldNavigateViaGrid, tappedGridContact: $tappedGridContact) + } + } + + Section("All CatchUps") { + ForEach(selectedContacts) { contact in + NavigationLink(destination: DetailScreen(contact: contact)) { + ContactRowView(contact: contact) + } + } + .onDelete(perform: removePendingNotificationsAndDeleteContact) + } + } else { + Text("No CatchUps yet! Tap the 'Add Contacts' button to add some.") + } + } + .refreshable { + NotificationHelper.resetNotifications(for: selectedContacts, delayTime: 0) + ContactHelper.updateSelectedContacts(selectedContacts) + } + + .onChange(of: contactPicker.chosenContacts) { initialContacts, contacts in + if !contacts.isEmpty { + saveSelectedContact(for: contacts) + } + contactPicker.chosenContacts = [] + } + + Button { + openContactPicker() + } label: { + OpenContactPickerButtonView() + } + } + .navigationBarTitle("CatchUp") + + .onAppear { + clearNotificationBadgeAndCheckForUpdate() + + if isColdLaunch { + isColdLaunch = false + NotificationHelper.requestAuthorizationForNotifications() + + if timesUserHasLaunchedApp > 5 && Int.random(in: 1...3) == 2 { + requestReview() + } + + NotificationHelper.resetNotifications(for: selectedContacts, delayTime: 3) + timesUserHasLaunchedApp += 1 + } + } + + .onChange(of: scenePhase) { initialPhase, newPhase in + if newPhase == .active { + Utils.clearAppIconNotificationBadge() + updateNextNotificationTime(for: selectedContacts) + } + } + + .sheet(isPresented: $isShowingUpdatesSheet) { + UpdatesScreen() + } + + .toolbar { + ToolbarItem(placement: .topBarLeading) { + EditButton() + .foregroundStyle(.blue) + } + + ToolbarItem(placement: .topBarTrailing) { + Button { + isShowingAboutSheet = true + } label: { + Image(systemName: "person.crop.square") + .foregroundStyle(.blue) + } + .sheet(isPresented: $isShowingAboutSheet) { + AboutScreen() + } + } + } + + .navigationDestination(isPresented: $shouldNavigateViaGrid) { + if let tappedGridContact { + DetailScreen(contact: tappedGridContact) + } + } + } + + @MainActor + func openContactPicker() { + let contactPicker = CNContactPickerViewController() + contactPicker.delegate = self.contactPicker + let scenes = UIApplication.shared.connectedScenes + let windowScenes = scenes.first as? UIWindowScene + let window = windowScenes?.windows.first + window?.rootViewController?.present(contactPicker, animated: true, completion: nil) + } + + func updateNextNotificationTime(for contacts: [SelectedContact]) { + print("updating next notification time for all contacts") + for contact in contacts { + let nextNotificationDateTime = NotificationHelper.getNextNotificationDateFor(contact: contact) + contact.next_notification_date_time = nextNotificationDateTime + } + } + + // save selected contacts and their properties to SwiftData + func saveSelectedContact(for contacts: [CNContact]) { + for contact in contacts { + let contactName = ContactHelper.getContactName(for: contact) + if !contactAlreadyAdded(name: contactName) { + let selectedContact = ContactHelper.createSelectedContact(contact: contact) + modelContext.insert(selectedContact) + } + } + } + + func contactAlreadyAdded(name: String) -> Bool { + for contact in selectedContacts { + if contact.name == name { + return true + } + } + return false + } + + func clearNotificationBadgeAndCheckForUpdate() { + Utils.fetchAvailableIAPs() + Utils.clearAppIconNotificationBadge() + + checkForUpdate() + } + + func removePendingNotificationsAndDeleteContact(at offsets: IndexSet) { + for index in offsets { + let contact = selectedContacts[index] + + NotificationHelper.removeExistingNotifications(for: contact) + modelContext.delete(contact) + } + } + + func checkForUpdate() { + let latestVersion = Utils.getCurrentAppVersion() + print("latest version: \(latestVersion)") + + if savedVersion == latestVersion { + print("App is up to date!") + } else { + if Utils.updateIsMajor() && timesUserHasLaunchedApp > 0 { + // Toggle to show UpdatesScreen as a sheet + print("Major update detected, showing UpdatesScreen...") + isShowingUpdatesSheet = true + } + savedVersion = latestVersion + } + } +} + +#Preview { + HomeScreen() +} diff --git a/CatchUp-SwiftUI/Views/NoContactSelectedScreen.swift b/CatchUp-SwiftUI/Views/NoContactSelectedScreen.swift new file mode 100644 index 0000000..a880bdf --- /dev/null +++ b/CatchUp-SwiftUI/Views/NoContactSelectedScreen.swift @@ -0,0 +1,40 @@ +// +// NoContactSelectedScreen.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 4/1/24. +// Copyright © 2024 Token Solutions. All rights reserved. +// + +import SwiftUI + +struct NoContactSelectedScreen: View { + var body: some View { + HStack { + Spacer() + + VStack { + Spacer() + + Image("CatchUp") + .resizable() + .frame(width: 100, height: 100) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .shadow(radius: 15) + .padding(.bottom) + + Text("Select a contact from the left sidebar to get started.") + .fontWeight(.semibold) + + Spacer() + Spacer() + } + + Spacer() + } + } +} + +#Preview { + NoContactSelectedScreen() +} diff --git a/CatchUp-SwiftUI/Views/UpdatesScreen.swift b/CatchUp-SwiftUI/Views/UpdatesScreen.swift new file mode 100644 index 0000000..19adf58 --- /dev/null +++ b/CatchUp-SwiftUI/Views/UpdatesScreen.swift @@ -0,0 +1,68 @@ +// +// UpdatesScreen.swift +// CatchUp-SwiftUI +// +// Created by Ryan Token on 4/29/20. +// Copyright © 2020 Token Solutions. All rights reserved. +// + +import SwiftUI + +struct UpdatesScreen: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 10) { + Group { + Spacer() + .frame(height: 10) + + Text("New Update") + .font(.largeTitle) + .bold() + .foregroundStyle(.orange) + + Text("Version \(Utils.getCurrentAppVersion())") + .font(.headline) + .foregroundStyle(.blue) + + Text("Release Notes:") + .font(.headline) + + Divider() + Spacer() + } + + Group { + Text("– A grid of your next CatchUps") + + Spacer() + + Text("– Pull-to-refresh photo & contact information for your selected contacts") + + Spacer() + + Text("– Unread indicators for contacts it's time to CatchUp with") + + Spacer() + + Text("– Automatic cloud syncing with other Apple devices") + + Spacer() + + Text("– UI redesign") + + Spacer() + + Text("– Significant under-the-hood improvements") + } + + Spacer() + } + } + .padding([.top, .horizontal]) + } +} + +#Preview { + UpdatesScreen() +} diff --git a/SelectedContact+CoreDataClass.swift b/SelectedContact+CoreDataClass.swift deleted file mode 100644 index 8e33d84..0000000 --- a/SelectedContact+CoreDataClass.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// SelectedContact+CoreDataClass.swift -// CatchUp-SwiftUI -// -// Created by Ryan Token on 4/17/20. -// Copyright © 2020 Token Solutions. All rights reserved. -// -// - -import Foundation -import CoreData - -@objc(SelectedContact) -public class SelectedContact: NSManagedObject, Identifiable { - -} diff --git a/SelectedContact+CoreDataProperties.swift b/SelectedContact+CoreDataProperties.swift deleted file mode 100644 index 4c6ba3b..0000000 --- a/SelectedContact+CoreDataProperties.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// SelectedContact+CoreDataProperties.swift -// CatchUp-SwiftUI -// -// Created by Ryan Token on 4/17/20. -// Copyright © 2020 Token Solutions. All rights reserved. -// -// - -import Foundation -import CoreData - - -extension SelectedContact { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "SelectedContact") - } - - @NSManaged public var address: String - @NSManaged public var anniversary: String - @NSManaged public var anniversary_notification_id: UUID - @NSManaged public var birthday: String - @NSManaged public var birthday_notification_id: UUID - @NSManaged public var email: String - @NSManaged public var id: UUID - @NSManaged public var name: String - @NSManaged public var notification_identifier: UUID - @NSManaged public var notification_preference: Int16 - @NSManaged public var notification_preference_hour: Int16 - @NSManaged public var notification_preference_minute: Int16 - @NSManaged public var notification_preference_weekday: Int16 - @NSManaged public var phone: String - @NSManaged public var picture: String - @NSManaged public var secondary_address: String - @NSManaged public var secondary_email: String - @NSManaged public var secondary_phone: String - @NSManaged public var notification_preference_custom_month: Int16 - @NSManaged public var notification_preference_custom_day: Int16 - @NSManaged public var notification_preference_custom_year: Int16 - -}