diff --git a/.github/workflows/ci-pull-request.yml b/.github/workflows/ci-pull-request.yml deleted file mode 100644 index cdc2195..0000000 --- a/.github/workflows/ci-pull-request.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: CI - Pull Request -on: - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - build-swiftpm: - name: Build SwiftPM - uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm.yml@main - strategy: - matrix: - packages: [OversizeKit, OversizeCalendarKit, OversizeContactsKit, OversizeLocationKit, OversizeNoticeKit, OversizeNotificationKit, OversizeOnboardingKit, OversizePhotoKit] - with: - package: ${{ matrix.packages }} - secrets: inherit - - build-example: - name: Build Example - needs: build-swiftpm - uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main - strategy: - matrix: - destination: ['platform=iOS Simulator,name=iPhone 15 Pro,OS=17.2', 'platform=iOS Simulator,name=iPad Pro (12.9-inch) (6th generation),OS=17.2'] - with: - path: AppExample/Example - scheme: Example - destination: ${{ matrix.destination }} - secrets: inherit - -# tests: -# name: Test -# needs: build-example -# uses: oversizedev/GithubWorkflows/.github/workflows/test.yml@main -# secrets: inherit diff --git a/.github/workflows/ci-push.yml b/.github/workflows/ci-push.yml deleted file mode 100644 index 9b09524..0000000 --- a/.github/workflows/ci-push.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: CI - Push -on: - pull_request: - types: - - closed - branches: - - main - workflow_dispatch: - -jobs: - - build-swiftpm: - name: Build SwiftPM - uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm.yml@main - strategy: - matrix: - packages: [OversizeKit, OversizeCalendarKit, OversizeContactsKit, OversizeLocationKit, OversizeNoticeKit, OversizeNotificationKit, OversizeOnboardingKit, OversizePhotoKit] - with: - package: ${{ matrix.packages }} - secrets: inherit - - build-example: - name: Build Example - needs: build-swiftpm - uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main - strategy: - matrix: - destination: ['platform=iOS Simulator,name=iPhone 15 Pro,OS=17.2', 'platform=iOS Simulator,name=iPad Pro (12.9-inch) (6th generation),OS=17.2'] - with: - path: AppExample/Example - scheme: Example - destination: ${{ matrix.destination }} - secrets: inherit - -# tests: -# name: Test -# needs: build-example -# uses: oversizedev/GithubWorkflows/.github/workflows/test.yml@main -# secrets: inherit - - bump: - name: Bump version - needs: build-example - uses: oversizedev/GithubWorkflows/.github/workflows/bump.yml@main - secrets: inherit - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b6221dc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +name: CI + +on: + push: + branches: + - '**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-oversize-kit: + name: Build OversizeKit + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm-all-platforms.yml@main + with: + package: OversizeKit + secrets: inherit + + build-calendar-kit: + name: Build CalendarKit + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm-all-platforms.yml@main + with: + package: OversizeCalendarKit + secrets: inherit + + build-contacts-kit: + name: Build OversizeContactsKit + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm-all-platforms.yml@main + with: + package: OversizeContactsKit + secrets: inherit + + build-location-kit: + name: Build LocationKit + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm-all-platforms.yml@main + with: + package: OversizeLocationKit + secrets: inherit + + build-notice-kit: + name: Build NoticeKit + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm-all-platforms.yml@main + with: + package: OversizeNoticeKit + secrets: inherit + + build-notification-kit: + name: Build NotificationKit + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm-all-platforms.yml@main + with: + package: OversizeNotificationKit + secrets: inherit + + build-onboarding-kit: + name: Build OnboardingKit + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm-all-platforms.yml@main + with: + package: OversizeOnboardingKit + secrets: inherit + + build-photo-kit: + name: Build PhotoKit + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm-all-platforms.yml@main + with: + package: OversizePhotoKit + secrets: inherit + + build-example: + name: Build Example + needs: + - build-oversize-kit + - build-calendar-kit + - build-contacts-kit + - build-location-kit + - build-notice-kit + - build-notification-kit + - build-onboarding-kit + - build-photo-kit + uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main + with: + path: AppExample/Example + scheme: Example + destination: platform=iOS Simulator,name=iPhone 16,OS=18.1 + secrets: inherit + +# tests: +# name: Test +# needs: build-swiftpm +# uses: oversizedev/GithubWorkflows/.github/workflows/test.yml@main +# secrets: inherit + + bump: + name: Bump version + needs: + - build-example + if: github.ref == 'refs/heads/main' + uses: oversizedev/GithubWorkflows/.github/workflows/bump.yml@main + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3065293..988e905 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - "*.*.*" jobs: - build: + release: name: Create release runs-on: ubuntu-latest steps: diff --git a/AppExample/Example (watchOS) Watch App/Assets.xcassets/AccentColor.colorset/Contents.json b/AppExample/Example (watchOS) Watch App/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/AppExample/Example (watchOS) Watch App/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppExample/Example (watchOS) Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json b/AppExample/Example (watchOS) Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..49c81cd --- /dev/null +++ b/AppExample/Example (watchOS) Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppExample/Example (watchOS) Watch App/Assets.xcassets/Contents.json b/AppExample/Example (watchOS) Watch App/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/AppExample/Example (watchOS) Watch App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppExample/Example (watchOS) Watch App/ContentView.swift b/AppExample/Example (watchOS) Watch App/ContentView.swift new file mode 100644 index 0000000..b173e94 --- /dev/null +++ b/AppExample/Example (watchOS) Watch App/ContentView.swift @@ -0,0 +1,30 @@ +// +// Copyright © 2024 Alexander Romanov +// ContentView.swift, created on 19.05.2024 +// + +import OversizeCalendarKit +import OversizeContactsKit +import OversizeKit +import OversizeLocationKit +import OversizeNoticeKit +import OversizeNotificationKit +import OversizeOnboardingKit +import OversizePhotoKit +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/AppExample/Example (watchOS) Watch App/Example__watchOS_App.swift b/AppExample/Example (watchOS) Watch App/Example__watchOS_App.swift new file mode 100644 index 0000000..c00dd47 --- /dev/null +++ b/AppExample/Example (watchOS) Watch App/Example__watchOS_App.swift @@ -0,0 +1,21 @@ +// +// Copyright © 2024 Alexander Romanov +// Example__watchOS_App.swift, created on 19.05.2024 +// + +import Factory +import OversizeKit +import OversizeServices +import OversizeUI +import SwiftUI + +@main +struct Example__watchOS__Watch_AppApp: App { + @Injected(\.appStateService) var appStateService: AppStateService + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/AppExample/Example (watchOS) Watch App/Preview Content/Preview Assets.xcassets/Contents.json b/AppExample/Example (watchOS) Watch App/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/AppExample/Example (watchOS) Watch App/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppExample/Example.xcodeproj/project.pbxproj b/AppExample/Example.xcodeproj/project.pbxproj index a8d543f..d0a2ce7 100644 --- a/AppExample/Example.xcodeproj/project.pbxproj +++ b/AppExample/Example.xcodeproj/project.pbxproj @@ -22,19 +22,57 @@ 840CD68E2AC0E39D00C6AAD0 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840CD68D2AC0E39D00C6AAD0 /* ExampleApp.swift */; }; 840CD6902AC0E3A600C6AAD0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 840CD68F2AC0E3A600C6AAD0 /* Assets.xcassets */; }; 840CD6932AC0E3A600C6AAD0 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 840CD6922AC0E3A600C6AAD0 /* Preview Assets.xcassets */; }; - 840CD69E2AC0E43000C6AAD0 /* OversizeCalendarKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD69D2AC0E43000C6AAD0 /* OversizeCalendarKit */; }; - 840CD6A02AC0E43000C6AAD0 /* OversizeContactsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD69F2AC0E43000C6AAD0 /* OversizeContactsKit */; }; - 840CD6A22AC0E43000C6AAD0 /* OversizeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD6A12AC0E43000C6AAD0 /* OversizeKit */; }; - 840CD6A42AC0E43000C6AAD0 /* OversizeLocationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD6A32AC0E43000C6AAD0 /* OversizeLocationKit */; }; - 840CD6A62AC0E43000C6AAD0 /* OversizeNoticeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD6A52AC0E43000C6AAD0 /* OversizeNoticeKit */; }; - 840CD6A82AC0E43000C6AAD0 /* OversizeNotificationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD6A72AC0E43000C6AAD0 /* OversizeNotificationKit */; }; - 840CD6AA2AC0E43000C6AAD0 /* OversizeOnboardingKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD6A92AC0E43000C6AAD0 /* OversizeOnboardingKit */; }; - 840CD6AC2AC0E43000C6AAD0 /* OversizePhotoKit in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD6AB2AC0E43000C6AAD0 /* OversizePhotoKit */; }; 840CD6AF2AC0E44E00C6AAD0 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 840CD6AE2AC0E44E00C6AAD0 /* Factory */; }; 840CD6B12AC0E6E200C6AAD0 /* Products.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 840CD6B02AC0E6E200C6AAD0 /* Products.storekit */; }; - 845A59332BA4FD2B00988D52 /* OversizeModels in Frameworks */ = {isa = PBXBuildFile; productRef = 845A59322BA4FD2B00988D52 /* OversizeModels */; }; + 84664C312BF9FD6400A24148 /* Example (watchOS) Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 84664C302BF9FD6400A24148 /* Example (watchOS) Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 84664C362BF9FD6400A24148 /* Example__watchOS_App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84664C352BF9FD6400A24148 /* Example__watchOS_App.swift */; }; + 84664C382BF9FD6400A24148 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84664C372BF9FD6400A24148 /* ContentView.swift */; }; + 84664C3A2BF9FD6500A24148 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84664C392BF9FD6500A24148 /* Assets.xcassets */; }; + 84664C3D2BF9FD6500A24148 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84664C3C2BF9FD6500A24148 /* Preview Assets.xcassets */; }; + 84664C472BF9FDC200A24148 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 84664C462BF9FDC200A24148 /* Factory */; }; + 848479442BF9FF94003FC3FC /* OversizeCalendarKit in Frameworks */ = {isa = PBXBuildFile; productRef = 848479432BF9FF94003FC3FC /* OversizeCalendarKit */; }; + 848479462BF9FF94003FC3FC /* OversizeContactsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 848479452BF9FF94003FC3FC /* OversizeContactsKit */; }; + 848479482BF9FF94003FC3FC /* OversizeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 848479472BF9FF94003FC3FC /* OversizeKit */; }; + 8484794A2BF9FF94003FC3FC /* OversizeLocationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 848479492BF9FF94003FC3FC /* OversizeLocationKit */; }; + 8484794C2BF9FF94003FC3FC /* OversizeNoticeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8484794B2BF9FF94003FC3FC /* OversizeNoticeKit */; }; + 8484794E2BF9FF94003FC3FC /* OversizeNotificationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8484794D2BF9FF94003FC3FC /* OversizeNotificationKit */; }; + 848479502BF9FF94003FC3FC /* OversizeOnboardingKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8484794F2BF9FF94003FC3FC /* OversizeOnboardingKit */; }; + 848479522BF9FF94003FC3FC /* OversizePhotoKit in Frameworks */ = {isa = PBXBuildFile; productRef = 848479512BF9FF94003FC3FC /* OversizePhotoKit */; }; + 848479542BF9FFA4003FC3FC /* OversizeCalendarKit in Frameworks */ = {isa = PBXBuildFile; productRef = 848479532BF9FFA4003FC3FC /* OversizeCalendarKit */; }; + 848479562BF9FFA4003FC3FC /* OversizeContactsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 848479552BF9FFA4003FC3FC /* OversizeContactsKit */; }; + 848479582BF9FFA4003FC3FC /* OversizeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 848479572BF9FFA4003FC3FC /* OversizeKit */; }; + 8484795A2BF9FFA4003FC3FC /* OversizeLocationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 848479592BF9FFA4003FC3FC /* OversizeLocationKit */; }; + 8484795C2BF9FFA4003FC3FC /* OversizeNoticeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8484795B2BF9FFA4003FC3FC /* OversizeNoticeKit */; }; + 8484795E2BF9FFA4003FC3FC /* OversizeNotificationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8484795D2BF9FFA4003FC3FC /* OversizeNotificationKit */; }; + 848479602BF9FFA4003FC3FC /* OversizeOnboardingKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8484795F2BF9FFA4003FC3FC /* OversizeOnboardingKit */; }; + 848479622BF9FFA4003FC3FC /* OversizePhotoKit in Frameworks */ = {isa = PBXBuildFile; productRef = 848479612BF9FFA4003FC3FC /* OversizePhotoKit */; }; + 848479652BFA0E64003FC3FC /* TestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848479642BFA0E64003FC3FC /* TestView.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 84664C322BF9FD6400A24148 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 840CD6592AC0E39D00C6AAD0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 84664C2F2BF9FD6400A24148; + remoteInfo = "Example (watchOS) Watch App"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 84664C412BF9FD6500A24148 /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + 84664C312BF9FD6400A24148 /* Example (watchOS) Watch App.app in Embed Watch Content */, + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 840CD6632AC0E39D00C6AAD0 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 840CD6682AC0E39D00C6AAD0 /* AppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsView.swift; sourceTree = ""; }; @@ -58,6 +96,15 @@ 840CD6922AC0E3A600C6AAD0 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 840CD6942AC0E3A600C6AAD0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 840CD6B02AC0E6E200C6AAD0 /* Products.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Products.storekit; sourceTree = ""; }; + 84664C2B2BF9FD6400A24148 /* Example (watchOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example (watchOS).app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 84664C302BF9FD6400A24148 /* Example (watchOS) Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example (watchOS) Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 84664C352BF9FD6400A24148 /* Example__watchOS_App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example__watchOS_App.swift; sourceTree = ""; }; + 84664C372BF9FD6400A24148 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 84664C392BF9FD6500A24148 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 84664C3C2BF9FD6500A24148 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 848479642BFA0E64003FC3FC /* TestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestView.swift; sourceTree = ""; }; + 84DE88812BF9FEED00CCF37A /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Package.swift; path = ../Package.swift; sourceTree = ""; }; + 84DE88822BF9FF5100CCF37A /* OversizeKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = OversizeKit; path = ..; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -65,16 +112,31 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 840CD6AC2AC0E43000C6AAD0 /* OversizePhotoKit in Frameworks */, + 848479622BF9FFA4003FC3FC /* OversizePhotoKit in Frameworks */, + 848479542BF9FFA4003FC3FC /* OversizeCalendarKit in Frameworks */, + 8484795E2BF9FFA4003FC3FC /* OversizeNotificationKit in Frameworks */, 840CD6AF2AC0E44E00C6AAD0 /* Factory in Frameworks */, - 840CD69E2AC0E43000C6AAD0 /* OversizeCalendarKit in Frameworks */, - 840CD6A82AC0E43000C6AAD0 /* OversizeNotificationKit in Frameworks */, - 840CD6A22AC0E43000C6AAD0 /* OversizeKit in Frameworks */, - 840CD6A42AC0E43000C6AAD0 /* OversizeLocationKit in Frameworks */, - 840CD6A62AC0E43000C6AAD0 /* OversizeNoticeKit in Frameworks */, - 845A59332BA4FD2B00988D52 /* OversizeModels in Frameworks */, - 840CD6A02AC0E43000C6AAD0 /* OversizeContactsKit in Frameworks */, - 840CD6AA2AC0E43000C6AAD0 /* OversizeOnboardingKit in Frameworks */, + 848479582BF9FFA4003FC3FC /* OversizeKit in Frameworks */, + 8484795A2BF9FFA4003FC3FC /* OversizeLocationKit in Frameworks */, + 8484795C2BF9FFA4003FC3FC /* OversizeNoticeKit in Frameworks */, + 848479562BF9FFA4003FC3FC /* OversizeContactsKit in Frameworks */, + 848479602BF9FFA4003FC3FC /* OversizeOnboardingKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84664C2D2BF9FD6400A24148 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 848479522BF9FF94003FC3FC /* OversizePhotoKit in Frameworks */, + 848479442BF9FF94003FC3FC /* OversizeCalendarKit in Frameworks */, + 8484794E2BF9FF94003FC3FC /* OversizeNotificationKit in Frameworks */, + 84664C472BF9FDC200A24148 /* Factory in Frameworks */, + 848479482BF9FF94003FC3FC /* OversizeKit in Frameworks */, + 8484794A2BF9FF94003FC3FC /* OversizeLocationKit in Frameworks */, + 8484794C2BF9FF94003FC3FC /* OversizeNoticeKit in Frameworks */, + 848479462BF9FF94003FC3FC /* OversizeContactsKit in Frameworks */, + 848479502BF9FF94003FC3FC /* OversizeOnboardingKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -84,8 +146,11 @@ 840CD6582AC0E39D00C6AAD0 = { isa = PBXGroup; children = ( + 84DE88822BF9FF5100CCF37A /* OversizeKit */, 840CD6652AC0E39D00C6AAD0 /* Example */, + 84664C342BF9FD6400A24148 /* Example (watchOS) Watch App */, 840CD6642AC0E39D00C6AAD0 /* Products */, + 84664C452BF9FDC200A24148 /* Frameworks */, ); sourceTree = ""; }; @@ -93,6 +158,8 @@ isa = PBXGroup; children = ( 840CD6632AC0E39D00C6AAD0 /* Example.app */, + 84664C2B2BF9FD6400A24148 /* Example (watchOS).app */, + 84664C302BF9FD6400A24148 /* Example (watchOS) Watch App.app */, ); name = Products; sourceTree = ""; @@ -101,6 +168,7 @@ isa = PBXGroup; children = ( 840CD68D2AC0E39D00C6AAD0 /* ExampleApp.swift */, + 848479632BFA0E55003FC3FC /* Test */, 840CD6662AC0E39D00C6AAD0 /* Screens */, 840CD6782AC0E39D00C6AAD0 /* Router */, 840CD67D2AC0E39D00C6AAD0 /* Resources */, @@ -189,6 +257,41 @@ path = "Preview Content"; sourceTree = ""; }; + 84664C342BF9FD6400A24148 /* Example (watchOS) Watch App */ = { + isa = PBXGroup; + children = ( + 84664C352BF9FD6400A24148 /* Example__watchOS_App.swift */, + 84664C372BF9FD6400A24148 /* ContentView.swift */, + 84664C392BF9FD6500A24148 /* Assets.xcassets */, + 84664C3B2BF9FD6500A24148 /* Preview Content */, + ); + path = "Example (watchOS) Watch App"; + sourceTree = ""; + }; + 84664C3B2BF9FD6500A24148 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 84664C3C2BF9FD6500A24148 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 84664C452BF9FDC200A24148 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 84DE88812BF9FEED00CCF37A /* Package.swift */, + ); + name = Frameworks; + sourceTree = ""; + }; + 848479632BFA0E55003FC3FC /* Test */ = { + isa = PBXGroup; + children = ( + 848479642BFA0E64003FC3FC /* TestView.swift */, + ); + path = Test; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -206,21 +309,65 @@ ); name = Example; packageProductDependencies = ( - 840CD69D2AC0E43000C6AAD0 /* OversizeCalendarKit */, - 840CD69F2AC0E43000C6AAD0 /* OversizeContactsKit */, - 840CD6A12AC0E43000C6AAD0 /* OversizeKit */, - 840CD6A32AC0E43000C6AAD0 /* OversizeLocationKit */, - 840CD6A52AC0E43000C6AAD0 /* OversizeNoticeKit */, - 840CD6A72AC0E43000C6AAD0 /* OversizeNotificationKit */, - 840CD6A92AC0E43000C6AAD0 /* OversizeOnboardingKit */, - 840CD6AB2AC0E43000C6AAD0 /* OversizePhotoKit */, 840CD6AE2AC0E44E00C6AAD0 /* Factory */, - 845A59322BA4FD2B00988D52 /* OversizeModels */, + 848479532BF9FFA4003FC3FC /* OversizeCalendarKit */, + 848479552BF9FFA4003FC3FC /* OversizeContactsKit */, + 848479572BF9FFA4003FC3FC /* OversizeKit */, + 848479592BF9FFA4003FC3FC /* OversizeLocationKit */, + 8484795B2BF9FFA4003FC3FC /* OversizeNoticeKit */, + 8484795D2BF9FFA4003FC3FC /* OversizeNotificationKit */, + 8484795F2BF9FFA4003FC3FC /* OversizeOnboardingKit */, + 848479612BF9FFA4003FC3FC /* OversizePhotoKit */, ); productName = Example; productReference = 840CD6632AC0E39D00C6AAD0 /* Example.app */; productType = "com.apple.product-type.application"; }; + 84664C2A2BF9FD6400A24148 /* Example (watchOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 84664C422BF9FD6500A24148 /* Build configuration list for PBXNativeTarget "Example (watchOS)" */; + buildPhases = ( + 84664C292BF9FD6400A24148 /* Resources */, + 84664C412BF9FD6500A24148 /* Embed Watch Content */, + ); + buildRules = ( + ); + dependencies = ( + 84664C332BF9FD6400A24148 /* PBXTargetDependency */, + ); + name = "Example (watchOS)"; + productName = "Example (watchOS)"; + productReference = 84664C2B2BF9FD6400A24148 /* Example (watchOS).app */; + productType = "com.apple.product-type.application.watchapp2-container"; + }; + 84664C2F2BF9FD6400A24148 /* Example (watchOS) Watch App */ = { + isa = PBXNativeTarget; + buildConfigurationList = 84664C3E2BF9FD6500A24148 /* Build configuration list for PBXNativeTarget "Example (watchOS) Watch App" */; + buildPhases = ( + 84664C2C2BF9FD6400A24148 /* Sources */, + 84664C2D2BF9FD6400A24148 /* Frameworks */, + 84664C2E2BF9FD6400A24148 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Example (watchOS) Watch App"; + packageProductDependencies = ( + 84664C462BF9FDC200A24148 /* Factory */, + 848479432BF9FF94003FC3FC /* OversizeCalendarKit */, + 848479452BF9FF94003FC3FC /* OversizeContactsKit */, + 848479472BF9FF94003FC3FC /* OversizeKit */, + 848479492BF9FF94003FC3FC /* OversizeLocationKit */, + 8484794B2BF9FF94003FC3FC /* OversizeNoticeKit */, + 8484794D2BF9FF94003FC3FC /* OversizeNotificationKit */, + 8484794F2BF9FF94003FC3FC /* OversizeOnboardingKit */, + 848479512BF9FF94003FC3FC /* OversizePhotoKit */, + ); + productName = "Example (watchOS) Watch App"; + productReference = 84664C302BF9FD6400A24148 /* Example (watchOS) Watch App.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -228,12 +375,18 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1500; + LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1500; TargetAttributes = { 840CD6622AC0E39D00C6AAD0 = { CreatedOnToolsVersion = 15.0; }; + 84664C2A2BF9FD6400A24148 = { + CreatedOnToolsVersion = 15.4; + }; + 84664C2F2BF9FD6400A24148 = { + CreatedOnToolsVersion = 15.4; + }; }; }; buildConfigurationList = 840CD65C2AC0E39D00C6AAD0 /* Build configuration list for PBXProject "Example" */; @@ -255,6 +408,8 @@ projectRoot = ""; targets = ( 840CD6622AC0E39D00C6AAD0 /* Example */, + 84664C2A2BF9FD6400A24148 /* Example (watchOS) */, + 84664C2F2BF9FD6400A24148 /* Example (watchOS) Watch App */, ); }; /* End PBXProject section */ @@ -271,6 +426,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 84664C292BF9FD6400A24148 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84664C2E2BF9FD6400A24148 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 84664C3D2BF9FD6500A24148 /* Preview Assets.xcassets in Resources */, + 84664C3A2BF9FD6500A24148 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -289,12 +460,30 @@ 840CD66D2AC0E39D00C6AAD0 /* AppSettingsPageView.swift in Sources */, 840CD67C2AC0E39D00C6AAD0 /* Screens.swift in Sources */, 840CD6722AC0E39D00C6AAD0 /* OnboardingView.swift in Sources */, + 848479652BFA0E64003FC3FC /* TestView.swift in Sources */, 840CD66B2AC0E39D00C6AAD0 /* AppSettingsViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 84664C2C2BF9FD6400A24148 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 84664C382BF9FD6400A24148 /* ContentView.swift in Sources */, + 84664C362BF9FD6400A24148 /* Example__watchOS_App.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 84664C332BF9FD6400A24148 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 84664C2F2BF9FD6400A24148 /* Example (watchOS) Watch App */; + targetProxy = 84664C322BF9FD6400A24148 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 840CD6952AC0E3A600C6AAD0 /* Debug */ = { isa = XCBuildConfiguration; @@ -432,17 +621,22 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = app.oversize.Example; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 18.0; }; name = Debug; }; @@ -463,17 +657,112 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = app.oversize.Example; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 18.0; + }; + name = Release; + }; + 84664C3F2BF9FD6500A24148 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Example (watchOS) Watch App/Preview Content\""; + DEVELOPMENT_TEAM = ER582ZK85C; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "Example (watchOS)"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKWatchOnly = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "app.oversize.Example--watchOS-.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Debug; + }; + 84664C402BF9FD6500A24148 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Example (watchOS) Watch App/Preview Content\""; + DEVELOPMENT_TEAM = ER582ZK85C; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "Example (watchOS)"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKWatchOnly = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "app.oversize.Example--watchOS-.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Release; + }; + 84664C432BF9FD6500A24148 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ER582ZK85C; + INFOPLIST_KEY_CFBundleDisplayName = "Example (watchOS)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "app.oversize.Example--watchOS-"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 84664C442BF9FD6500A24148 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ER582ZK85C; + INFOPLIST_KEY_CFBundleDisplayName = "Example (watchOS)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "app.oversize.Example--watchOS-"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -498,6 +787,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 84664C3E2BF9FD6500A24148 /* Build configuration list for PBXNativeTarget "Example (watchOS) Watch App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 84664C3F2BF9FD6500A24148 /* Debug */, + 84664C402BF9FD6500A24148 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 84664C422BF9FD6500A24148 /* Build configuration list for PBXNativeTarget "Example (watchOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 84664C432BF9FD6500A24148 /* Debug */, + 84664C442BF9FD6500A24148 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ @@ -513,7 +820,7 @@ repositoryURL = "https://github.com/hmlongco/Factory"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.2.0; + minimumVersion = 2.1.3; }; }; 845A59312BA4FD2B00988D52 /* XCRemoteSwiftPackageReference "OversizeModels" */ = { @@ -527,47 +834,79 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 840CD69D2AC0E43000C6AAD0 /* OversizeCalendarKit */ = { + 840CD6AE2AC0E44E00C6AAD0 /* Factory */ = { + isa = XCSwiftPackageProductDependency; + package = 840CD6AD2AC0E44E00C6AAD0 /* XCRemoteSwiftPackageReference "Factory" */; + productName = Factory; + }; + 84664C462BF9FDC200A24148 /* Factory */ = { + isa = XCSwiftPackageProductDependency; + package = 840CD6AD2AC0E44E00C6AAD0 /* XCRemoteSwiftPackageReference "Factory" */; + productName = Factory; + }; + 848479432BF9FF94003FC3FC /* OversizeCalendarKit */ = { isa = XCSwiftPackageProductDependency; productName = OversizeCalendarKit; }; - 840CD69F2AC0E43000C6AAD0 /* OversizeContactsKit */ = { + 848479452BF9FF94003FC3FC /* OversizeContactsKit */ = { isa = XCSwiftPackageProductDependency; productName = OversizeContactsKit; }; - 840CD6A12AC0E43000C6AAD0 /* OversizeKit */ = { + 848479472BF9FF94003FC3FC /* OversizeKit */ = { isa = XCSwiftPackageProductDependency; productName = OversizeKit; }; - 840CD6A32AC0E43000C6AAD0 /* OversizeLocationKit */ = { + 848479492BF9FF94003FC3FC /* OversizeLocationKit */ = { isa = XCSwiftPackageProductDependency; productName = OversizeLocationKit; }; - 840CD6A52AC0E43000C6AAD0 /* OversizeNoticeKit */ = { + 8484794B2BF9FF94003FC3FC /* OversizeNoticeKit */ = { isa = XCSwiftPackageProductDependency; productName = OversizeNoticeKit; }; - 840CD6A72AC0E43000C6AAD0 /* OversizeNotificationKit */ = { + 8484794D2BF9FF94003FC3FC /* OversizeNotificationKit */ = { isa = XCSwiftPackageProductDependency; productName = OversizeNotificationKit; }; - 840CD6A92AC0E43000C6AAD0 /* OversizeOnboardingKit */ = { + 8484794F2BF9FF94003FC3FC /* OversizeOnboardingKit */ = { isa = XCSwiftPackageProductDependency; productName = OversizeOnboardingKit; }; - 840CD6AB2AC0E43000C6AAD0 /* OversizePhotoKit */ = { + 848479512BF9FF94003FC3FC /* OversizePhotoKit */ = { isa = XCSwiftPackageProductDependency; productName = OversizePhotoKit; }; - 840CD6AE2AC0E44E00C6AAD0 /* Factory */ = { + 848479532BF9FFA4003FC3FC /* OversizeCalendarKit */ = { isa = XCSwiftPackageProductDependency; - package = 840CD6AD2AC0E44E00C6AAD0 /* XCRemoteSwiftPackageReference "Factory" */; - productName = Factory; + productName = OversizeCalendarKit; + }; + 848479552BF9FFA4003FC3FC /* OversizeContactsKit */ = { + isa = XCSwiftPackageProductDependency; + productName = OversizeContactsKit; + }; + 848479572BF9FFA4003FC3FC /* OversizeKit */ = { + isa = XCSwiftPackageProductDependency; + productName = OversizeKit; + }; + 848479592BF9FFA4003FC3FC /* OversizeLocationKit */ = { + isa = XCSwiftPackageProductDependency; + productName = OversizeLocationKit; + }; + 8484795B2BF9FFA4003FC3FC /* OversizeNoticeKit */ = { + isa = XCSwiftPackageProductDependency; + productName = OversizeNoticeKit; }; - 845A59322BA4FD2B00988D52 /* OversizeModels */ = { + 8484795D2BF9FFA4003FC3FC /* OversizeNotificationKit */ = { isa = XCSwiftPackageProductDependency; - package = 845A59312BA4FD2B00988D52 /* XCRemoteSwiftPackageReference "OversizeModels" */; - productName = OversizeModels; + productName = OversizeNotificationKit; + }; + 8484795F2BF9FFA4003FC3FC /* OversizeOnboardingKit */ = { + isa = XCSwiftPackageProductDependency; + productName = OversizeOnboardingKit; + }; + 848479612BF9FFA4003FC3FC /* OversizePhotoKit */ = { + isa = XCSwiftPackageProductDependency; + productName = OversizePhotoKit; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/AppExample/Example.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate b/AppExample/Example.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate index 1cd4fb5..df3e6d4 100644 Binary files a/AppExample/Example.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate and b/AppExample/Example.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/AppExample/Example.xcodeproj/xcshareddata/xcschemes/Example (watchOS) Watch App.xcscheme b/AppExample/Example.xcodeproj/xcshareddata/xcschemes/Example (watchOS) Watch App.xcscheme new file mode 100644 index 0000000..06a4734 --- /dev/null +++ b/AppExample/Example.xcodeproj/xcshareddata/xcschemes/Example (watchOS) Watch App.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppExample/Example.xcodeproj/xcuserdata/admin.xcuserdatad/xcschemes/xcschememanagement.plist b/AppExample/Example.xcodeproj/xcuserdata/admin.xcuserdatad/xcschemes/xcschememanagement.plist index 268deb9..597af26 100644 --- a/AppExample/Example.xcodeproj/xcuserdata/admin.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/AppExample/Example.xcodeproj/xcuserdata/admin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -4,6 +4,11 @@ SchemeUserState + Example (watchOS) Watch App.xcscheme_^#shared#^_ + + orderHint + 2 + Example.xcscheme_^#shared#^_ orderHint @@ -17,6 +22,16 @@ primary + 84664C2A2BF9FD6400A24148 + + primary + + + 84664C2F2BF9FD6400A24148 + + primary + + diff --git a/AppExample/Example/ExampleApp.swift b/AppExample/Example/ExampleApp.swift index 0b518ba..32eedf3 100644 --- a/AppExample/Example/ExampleApp.swift +++ b/AppExample/Example/ExampleApp.swift @@ -97,6 +97,7 @@ struct ExampleApp: App { .environmentObject(appSettingsViewModel) .systemServices() } + #if os(iOS) .fullScreenCover(item: $router.fullScreenCover) { fullScreenCover in router.resolve(pathItem: fullScreenCover) .hud(router.hudText, isPresented: $router.isShowHud) @@ -104,6 +105,7 @@ struct ExampleApp: App { .environmentObject(appSettingsViewModel) .systemServices() } + #endif .alert(item: $router.alert) { $0.alert } .onOpenURL { router.handle($0) } .environmentObject(router) diff --git a/AppExample/Example/Screens/Onboarding/OnboardingView.swift b/AppExample/Example/Screens/Onboarding/OnboardingView.swift index c3b89b6..19d572e 100644 --- a/AppExample/Example/Screens/Onboarding/OnboardingView.swift +++ b/AppExample/Example/Screens/Onboarding/OnboardingView.swift @@ -35,11 +35,11 @@ struct OnboardingView: View { VStack(spacing: .medium) { Text("Welcome to\nExample") .largeTitle() - .onBackgroundHighEmphasisForegroundColor() + .onBackgroundPrimaryForeground() Text("Welcome text") .title2(.semibold) - .onBackgroundMediumEmphasisForegroundColor() + .onBackgroundSecondaryForeground() } .paddingContent(.horizontal) .multilineTextAlignment(.center) @@ -81,11 +81,11 @@ struct OnboardingView: View { VStack(spacing: .medium) { Text("Welcome to\nExample") .largeTitle() - .onBackgroundHighEmphasisForegroundColor() + .onBackgroundPrimaryForeground() Text("Welcome text") .title2(.semibold) - .onBackgroundMediumEmphasisForegroundColor() + .onBackgroundSecondaryForeground() } .paddingContent(.horizontal) .multilineTextAlignment(.center) @@ -123,11 +123,11 @@ struct OnboardingView: View { VStack(spacing: .medium) { Text("Example") .largeTitle() - .onBackgroundHighEmphasisForegroundColor() + .onBackgroundPrimaryForeground() Text("Welcome text") .title2(.semibold) - .onBackgroundMediumEmphasisForegroundColor() + .onBackgroundSecondaryForeground() } .paddingContent(.horizontal) .multilineTextAlignment(.center) diff --git a/AppExample/Example/Test/TestView.swift b/AppExample/Example/Test/TestView.swift new file mode 100644 index 0000000..9787784 --- /dev/null +++ b/AppExample/Example/Test/TestView.swift @@ -0,0 +1,24 @@ +// +// Copyright © 2024 Alexander Romanov +// TestView.swift, created on 19.05.2024 +// + +import OversizeCalendarKit +import OversizeContactsKit +import OversizeKit +import OversizeLocationKit +import OversizeNoticeKit +import OversizeNotificationKit +import OversizeOnboardingKit +import OversizePhotoKit +import SwiftUI + +struct TestView: View { + var body: some View { + Text("Hello, World!") + } +} + +#Preview { + TestView() +} diff --git a/AppExample/Package.swift b/AppExample/Package.swift new file mode 100644 index 0000000..9fdca6f --- /dev/null +++ b/AppExample/Package.swift @@ -0,0 +1,4 @@ +// swift-tools-version: 5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription diff --git a/Package.swift b/Package.swift index 5cdcfc2..1795262 100644 --- a/Package.swift +++ b/Package.swift @@ -1,18 +1,19 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import Foundation import PackageDescription -let productionDependencies: [PackageDescription.Package.Dependency] = [ +let remoteDependencies: [PackageDescription.Package.Dependency] = [ .package(url: "https://github.com/oversizedev/OversizeUI.git", .upToNextMajor(from: "3.0.2")), .package(url: "https://github.com/oversizedev/OversizeCore.git", .upToNextMajor(from: "1.3.0")), .package(url: "https://github.com/oversizedev/OversizeServices.git", .upToNextMajor(from: "1.4.0")), .package(url: "https://github.com/oversizedev/OversizeLocalizable.git", .upToNextMajor(from: "1.4.0")), - .package(url: "https://github.com/oversizedev/OversizeComponents.git", .upToNextMajor(from: "1.2.0")), + .package(url: "https://github.com/oversizedev/OversizeComponents.git", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/oversizedev/OversizeResources.git", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/oversizedev/OversizeNetwork.git", .upToNextMajor(from: "0.4.0")), .package(url: "https://github.com/oversizedev/OversizeModels.git", .upToNextMajor(from: "0.1.0")), + .package(url: "https://github.com/oversizedev/OversizeRouter.git", .upToNextMajor(from: "0.1.0")), .package(url: "https://github.com/hmlongco/Factory.git", .upToNextMajor(from: "2.1.3")), .package(url: "https://github.com/lorenzofiamingo/swiftui-cached-async-image.git", .upToNextMajor(from: "2.1.1")), ] @@ -26,19 +27,24 @@ let developmentDependencies: [PackageDescription.Package.Dependency] = [ .package(name: "OversizeResources", path: "../OversizeResources"), .package(name: "OversizeNetwork", path: "../OversizeNetwork"), .package(name: "OversizeModels", path: "../OversizeModels"), + .package(name: "OversizeRouter", path: "../OversizeRouter"), .package(url: "https://github.com/lorenzofiamingo/swiftui-cached-async-image.git", .upToNextMajor(from: "2.1.1")), .package(url: "https://github.com/hmlongco/Factory.git", .upToNextMajor(from: "2.1.3")), ] -let isProductionDependencies = ProcessInfo.processInfo.environment["RELEASE_DEPENDENCIES"] == "TRUE" +var dependencies: [PackageDescription.Package.Dependency] = remoteDependencies + +if ProcessInfo.processInfo.environment["BUILD_MODE"] == "DEV" { + dependencies = developmentDependencies +} let package = Package( name: "OversizeKit", platforms: [ - .iOS(.v15), - .macOS(.v13), - .tvOS(.v15), - .watchOS(.v9), + .iOS(.v17), + .macOS(.v14), + .tvOS(.v17), + .watchOS(.v10), ], products: [ .library(name: "OversizeKit", targets: ["OversizeKit"]), @@ -50,7 +56,7 @@ let package = Package( .library(name: "OversizeNotificationKit", targets: ["OversizeNotificationKit"]), .library(name: "OversizePhotoKit", targets: ["OversizePhotoKit"]), ], - dependencies: productionDependencies, + dependencies: dependencies, targets: [ .target( name: "OversizeKit", @@ -65,8 +71,9 @@ let package = Package( .product(name: "OversizeNotificationService", package: "OversizeServices"), .product(name: "OversizeModels", package: "OversizeModels"), .product(name: "OversizeNetwork", package: "OversizeNetwork"), + .product(name: "OversizeRouter", package: "OversizeRouter"), .product(name: "Factory", package: "Factory"), - .product(name: "CachedAsyncImage", package: "swiftui-cached-async-image") + .product(name: "CachedAsyncImage", package: "swiftui-cached-async-image"), ] ), .target( diff --git a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventView.swift b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventView.swift index b86aa0a..cfdb2ca 100644 --- a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventView.swift +++ b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventView.swift @@ -3,7 +3,9 @@ // CreateEventView.swift // -import EventKit +#if canImport(EventKit) + import EventKit +#endif import MapKit import OversizeCalendarService import OversizeComponents @@ -13,266 +15,317 @@ import OversizeModels import OversizeUI import SwiftUI -public struct CreateEventView: View { - @StateObject var viewModel: CreateEventViewModel - @Environment(\.dismiss) var dismiss - @FocusState private var focusedField: FocusField? +#if !os(tvOS) + public struct CreateEventView: View { + @StateObject var viewModel: CreateEventViewModel + @Environment(\.dismiss) var dismiss + @FocusState private var focusedField: FocusField? - public init(_ type: CreateEventType = .new(nil, calendar: nil)) { - _viewModel = StateObject(wrappedValue: CreateEventViewModel(type)) - } - - public var body: some View { - PageView { - content() - } - .leadingBar { - BarButton(.closeAction { - dismiss() - }) + public init(_ type: CreateEventType = .new(nil, calendar: nil)) { + _viewModel = StateObject(wrappedValue: CreateEventViewModel(type)) } - .trailingBar { - BarButton(.accent(L10n.Button.save, action: { - switch viewModel.type { - case .new: - Task { - _ = await viewModel.save() - dismiss() - } - case .update: - if viewModel.span == nil, viewModel.repitRule != .never { - viewModel.present(.span) - } else { + + public var body: some View { + PageView { + content() + } + .leadingBar { + BarButton(.closeAction { + dismiss() + }) + } + .trailingBar { + BarButton(.accent(L10n.Button.save, action: { + switch viewModel.type { + case .new: Task { _ = await viewModel.save() dismiss() } + case .update: + if viewModel.span == nil, viewModel.repitRule != .never { + viewModel.present(.span) + } else { + Task { + _ = await viewModel.save() + dismiss() + } + } + } + })) + .disabled(viewModel.title.isEmpty) + } + .titleLabel { + Button { viewModel.present(.calendar) } label: { + HStack(spacing: .xxxSmall) { + Circle() + .fill(Color(viewModel.calendar?.cgColor ?? CGColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1))) + .frame(width: 16, height: 16) + .padding(.xxxSmall) + + Text(viewModel.calendar?.title ?? "") + .padding(.trailing, .xxSmall) } } - })) - .disabled(viewModel.title.isEmpty) - } - .titleLabel { - Button { viewModel.present(.calendar) } label: { - HStack(spacing: .xxxSmall) { - Circle() - .fill(Color(viewModel.calendar?.cgColor ?? CGColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1))) - .frame(width: 16, height: 16) - .padding(.xxxSmall) - - Text(viewModel.calendar?.title ?? "") - .padding(.trailing, .xxSmall) - } + .buttonStyle(.tertiary) + .controlBorderShape(.capsule) + .controlSize(.mini) } - .buttonStyle(.tertiary) - .controlBorderShape(.capsule) - .controlSize(.mini) - } - .navigationBarDividerColor(Color.onSurfaceHighEmphasis.opacity(0.1)) - .safeAreaInset(edge: .bottom) { - bottomBar - } - .task { - await viewModel.fetchData() - } - .onAppear { - focusedField = .title - } - .sheet(item: $viewModel.sheet) { sheet in - resolveSheet(sheet: sheet) - } - .onChange(of: viewModel.span) { _ in - Task { - _ = await viewModel.save() - dismiss() + .navigationBarDividerColor(Color.onSurfacePrimary.opacity(0.1)) + .safeAreaInset(edge: .bottom) { + bottomBar + } + .task { + await viewModel.fetchData() + } + .onAppear { + focusedField = .title + } + .sheet(item: $viewModel.sheet) { sheet in + resolveSheet(sheet: sheet) + } + .onChange(of: viewModel.span) { _ in + Task { + _ = await viewModel.save() + dismiss() + } } } - } - @ViewBuilder - private func content() -> some View { - VStack(spacing: .small) { - TextField("Event name", text: $viewModel.title) - .title(.bold) - .focused($focusedField, equals: .title) - .onSurfaceHighEmphasisForegroundColor() - .padding(.bottom, .xxxSmall) - .padding(.horizontal, .small) + @ViewBuilder + private func content() -> some View { + VStack(spacing: .small) { + TextField("Event name", text: $viewModel.title) + .title(.bold) + .focused($focusedField, equals: .title) + .onSurfacePrimaryForeground() + .padding(.bottom, .xxxSmall) + .padding(.horizontal, .small) - textEditor + #if !os(watchOS) + textEditor + #endif - calendarButtons + calendarButtons - allDayEvent + allDayEvent - locationView + locationView - alarmView + alarmView - membersView + membersView - repitView + repitView + } + .padding(.horizontal, .small) + .padding(.vertical, .medium) } - .padding(.horizontal, .small) - .padding(.vertical, .medium) - } - var allDayEvent: some View { - Surface { - viewModel.isAllDay.toggle() - } label: { - HStack { - Text("All-day event") - .headline(.semibold) - .foregroundColor(.onSurfaceHighEmphasis) - .padding(.leading, .xxxSmall) + var allDayEvent: some View { + Surface { + viewModel.isAllDay.toggle() + } label: { + HStack { + Text("All-day event") + .headline(.semibold) + .foregroundColor(.onSurfacePrimary) + .padding(.leading, .xxxSmall) - Spacer() + Spacer() - Toggle(isOn: $viewModel.isAllDay) {} - .labelsHidden() + Toggle(isOn: $viewModel.isAllDay) {} + .labelsHidden() + } } + .surfaceBorderColor(Color.surfaceSecondary) + .surfaceBorderWidth(1) + .surfaceContentMargins(.init(horizontal: .xSmall, vertical: .xSmall)) + .controlRadius(.large) } - .surfaceBorderColor(Color.surfaceSecondary) - .surfaceBorderWidth(1) - .surfaceContentMargins(.init(horizontal: .xSmall, vertical: .xSmall)) - .controlRadius(.large) - } - var textEditor: some View { - VStack(spacing: 2) { - TextEditor(text: $viewModel.note) - .onSurfaceHighEmphasisForegroundColor() - .padding(.horizontal, .xSmall) - .padding(.vertical, .xxSmall) - .focused($focusedField, equals: .note) - .body(.medium) - .scrollContentBackground(.hidden) - .background { - #if os(iOS) - RoundedRectangleCorner(radius: 4, corners: [.bottomLeft, .bottomRight]) - .fillSurfaceSecondary() - .overlay(alignment: .topLeading) { - if viewModel.note.isEmpty { - Text("Note") - .body(.medium) - .onSurfaceDisabledForegroundColor() - .padding(.small) - } - } - #else - RoundedRectangle(cornerRadius: .small) - .fillSurfaceSecondary() - .overlay(alignment: .topLeading) { - if viewModel.note.isEmpty { - Text("Note") - .body(.medium) - .onSurfaceDisabledForegroundColor() - .padding(.small) - } - } - #endif + #if !os(watchOS) + var textEditor: some View { + VStack(spacing: 2) { + TextEditor(text: $viewModel.note) + .onSurfacePrimaryForeground() + .padding(.horizontal, .xSmall) + .padding(.vertical, .xxSmall) + .focused($focusedField, equals: .note) + .body(.medium) + .scrollContentBackground(.hidden) + .background { + #if os(iOS) + RoundedRectangleCorner(radius: 4, corners: [.bottomLeft, .bottomRight]) + .fillSurfaceSecondary() + .overlay(alignment: .topLeading) { + if viewModel.note.isEmpty { + Text("Note") + .body(.medium) + .onSurfaceTertiaryForeground() + .padding(.small) + } + } + #else + RoundedRectangle(cornerRadius: .small) + .fillSurfaceSecondary() + .overlay(alignment: .topLeading) { + if viewModel.note.isEmpty { + Text("Note") + .body(.medium) + .onSurfaceTertiaryForeground() + .padding(.small) + } + } + #endif + } + .frame(minHeight: 76) + + TextField("URL", text: $viewModel.url) + .focused($focusedField, equals: .url) + .onSurfacePrimaryForeground() + .body(.medium) + .padding(.horizontal, .small) + .padding(.vertical, 18) + .background { + #if os(iOS) + RoundedRectangleCorner(radius: 4, corners: [.topLeft, .topRight]) + .fillSurfaceSecondary() + #else + RoundedRectangle(cornerRadius: .small) + .fillSurfaceSecondary() + #endif + } } - .frame(minHeight: 76) - - TextField("URL", text: $viewModel.url) - .focused($focusedField, equals: .url) - .onSurfaceHighEmphasisForegroundColor() - .body(.medium) - .padding(.horizontal, .small) - .padding(.vertical, 18) - .background { - #if os(iOS) - RoundedRectangleCorner(radius: 4, corners: [.topLeft, .topRight]) - .fillSurfaceSecondary() - #else - RoundedRectangle(cornerRadius: .small) - .fillSurfaceSecondary() - #endif + .clipShape(RoundedRectangle(cornerRadius: .large, style: .continuous)) + } + #endif + + var repitView: some View { + Group { + if viewModel.repitRule != .never { + Surface { + Row(viewModel.repitRule.title, subtitle: repeatSubtitleText) { + viewModel.present(.repeat) + } leading: { + IconDeprecated(.refresh) + .iconColor(.onSurfacePrimary) + } + .rowClearButton(style: .onSurface) { + viewModel.repitRule = .never + viewModel.repitEndRule = .never + } + .surfaceContentMargins(.init(horizontal: .small, vertical: .medium)) + } + .surfaceBorderColor(Color.surfaceSecondary) + .surfaceBorderWidth(1) + .surfaceContentMargins(.zero) + .controlRadius(.large) } + } } - .clipShape(RoundedRectangle(cornerRadius: .large, style: .continuous)) - } - var repitView: some View { - Group { - if viewModel.repitRule != .never { - Surface { - Row(viewModel.repitRule.title, subtitle: repeatSubtitleText) { - viewModel.present(.repeat) - } leading: { - IconDeprecated(.refresh) - .iconColor(.onSurfaceHighEmphasis) - } - .rowClearButton(style: .onSurface) { - viewModel.repitRule = .never - viewModel.repitEndRule = .never + var membersView: some View { + Group { + if !viewModel.members.isEmpty { + Surface { + VStack(spacing: .zero) { + ForEach(viewModel.members, id: \.self) { email in + Row(email) { + viewModel.present(.invites) + } leading: { + IconDeprecated(.user) + .iconColor(.onSurfacePrimary) + } + .rowClearButton(style: .onSurface) { + viewModel.members.remove(email) + } + .rowContentMargins(.small) + .overlay(alignment: .bottomLeading) { + Rectangle() + .fillSurfaceSecondary() + .padding(.leading, 56) + .frame(height: 1) + } + } + } } - .surfaceContentMargins(.init(horizontal: .small, vertical: .medium)) + .surfaceBorderColor(Color.surfaceSecondary) + .surfaceBorderWidth(1) + .surfaceContentMargins(.zero) + .controlRadius(.large) } - .surfaceBorderColor(Color.surfaceSecondary) - .surfaceBorderWidth(1) - .surfaceContentMargins(.zero) - .controlRadius(.large) } } - } - var membersView: some View { - Group { - if !viewModel.members.isEmpty { - Surface { - VStack(spacing: .zero) { - ForEach(viewModel.members, id: \.self) { email in - Row(email) { - viewModel.present(.invites) - } leading: { - IconDeprecated(.user) - .iconColor(.onSurfaceHighEmphasis) - } - .rowClearButton(style: .onSurface) { - viewModel.members.remove(email) - } - .rowContentMargins(.small) - .overlay(alignment: .bottomLeading) { - Rectangle() - .fillSurfaceSecondary() - .padding(.leading, 56) - .frame(height: 1) + @ViewBuilder + var alarmView: some View { + Group { + if !viewModel.alarms.isEmpty { + Surface { + VStack(spacing: .zero) { + ForEach(viewModel.alarms) { alarm in + Row(alarm.title) { + viewModel.present(.alarm) + } leading: { + IconDeprecated(.bell) + .iconColor(.onSurfacePrimary) + } + .rowClearButton(style: .onSurface) { + viewModel.alarms.remove(alarm) + } + .surfaceContentMargins(.init(horizontal: .small, vertical: .medium)) + .overlay(alignment: .bottomLeading) { + Rectangle() + .fillSurfaceSecondary() + .padding(.leading, 56) + .frame(height: 1) + } } } } + .surfaceBorderColor(Color.surfaceSecondary) + .surfaceBorderWidth(1) + .surfaceContentMargins(.zero) + .controlRadius(.large) } - .surfaceBorderColor(Color.surfaceSecondary) - .surfaceBorderWidth(1) - .surfaceContentMargins(.zero) - .controlRadius(.large) } } - } - @ViewBuilder - var alarmView: some View { - Group { - if !viewModel.alarms.isEmpty { + @ViewBuilder + var locationView: some View { + if viewModel.locationName != nil || viewModel.location != nil { Surface { VStack(spacing: .zero) { - ForEach(viewModel.alarms) { alarm in - Row(alarm.title) { - viewModel.present(.alarm) - } leading: { - IconDeprecated(.bell) - .iconColor(.onSurfaceHighEmphasis) + if let locationName = viewModel.locationName { + VStack(spacing: .xxSmall) { + Row(locationName) { + viewModel.present(.location) + } leading: { + IconDeprecated(.mapPin) + .iconColor(.onSurfacePrimary) + } + .rowClearButton(style: .onSurface) { + viewModel.locationName = nil + viewModel.location = nil + } + .rowContentMargins(.init(horizontal: .small, vertical: .xSmall)) } - .rowClearButton(style: .onSurface) { - viewModel.alarms.remove(alarm) + } + + if let location = viewModel.location { + let region = MKCoordinateRegion(center: location, latitudinalMeters: 10000, longitudinalMeters: 10000) + let annotations = [MapPreviewPoint(name: "\(viewModel.locationName ?? "")", coordinate: location)] + Map(coordinateRegion: .constant(region), annotationItems: annotations) { + MapMarker(coordinate: $0.coordinate) } - .surfaceContentMargins(.init(horizontal: .small, vertical: .medium)) - .overlay(alignment: .bottomLeading) { - Rectangle() - .fillSurfaceSecondary() - .padding(.leading, 56) - .frame(height: 1) + .frame(height: 130) + .cornerRadius(.small) + .padding(.horizontal, .xxSmall) + .padding(.bottom, .xxSmall) + .onTapGesture { + focusedField = nil + viewModel.present(.location) } } } @@ -283,221 +336,174 @@ public struct CreateEventView: View { .controlRadius(.large) } } - } - @ViewBuilder - var locationView: some View { - if viewModel.locationName != nil || viewModel.location != nil { - Surface { - VStack(spacing: .zero) { - if let locationName = viewModel.locationName { - VStack(spacing: .xxSmall) { - Row(locationName) { - viewModel.present(.location) - } leading: { - IconDeprecated(.mapPin) - .iconColor(.onSurfaceHighEmphasis) - } - .rowClearButton(style: .onSurface) { - viewModel.locationName = nil - viewModel.location = nil - } - .rowContentMargins(.init(horizontal: .small, vertical: .xSmall)) - } - } - - if let location = viewModel.location { - let region = MKCoordinateRegion(center: location, latitudinalMeters: 10000, longitudinalMeters: 10000) - let annotations = [MapPreviewPoint(name: "\(viewModel.locationName ?? "")", coordinate: location)] - Map(coordinateRegion: .constant(region), annotationItems: annotations) { - MapMarker(coordinate: $0.coordinate) - } - .frame(height: 130) - .cornerRadius(.small) - .padding(.horizontal, .xxSmall) - .padding(.bottom, .xxSmall) - .onTapGesture { - focusedField = nil - viewModel.present(.location) - } - } - } + var repeatSubtitleText: String? { + switch viewModel.repitEndRule { + case .never: + return nil + case let .occurrenceCount(count): + return count > 1 ? "With \(count) repetitions" : "With 1 repetition" + case let .endDate(date): + return "Until \(date.formatted(date: .long, time: .omitted))" } - .surfaceBorderColor(Color.surfaceSecondary) - .surfaceBorderWidth(1) - .surfaceContentMargins(.zero) - .controlRadius(.large) - } - } - - var repeatSubtitleText: String? { - switch viewModel.repitEndRule { - case .never: - return nil - case let .occurrenceCount(count): - return count > 1 ? "With \(count) repetitions" : "With 1 repetition" - case let .endDate(date): - return "Until \(date.formatted(date: .long, time: .omitted))" } - } - - var calendarButtons: some View { - HStack(spacing: .small) { - Button { - focusedField = nil - viewModel.present(.startTime) - } label: { - VStack(alignment: .leading, spacing: .xxxSmall) { - Text("Starts") - .onSurfaceMediumEmphasisForegroundColor() - .subheadline(.semibold) - - Text(startDateText) - .onSurfaceHighEmphasisForegroundColor() - .headline(.semibold) - if !isCurrentYearEvent { - Text(viewModel.dateStart.formatted(.dateTime.year())) - .onSurfaceHighEmphasisForegroundColor() + var calendarButtons: some View { + HStack(spacing: .small) { + Button { + focusedField = nil + viewModel.present(.startTime) + } label: { + VStack(alignment: .leading, spacing: .xxxSmall) { + Text("Starts") + .onSurfaceSecondaryForeground() + .subheadline(.semibold) + + Text(startDateText) + .onSurfacePrimaryForeground() .headline(.semibold) + + if !isCurrentYearEvent { + Text(viewModel.dateStart.formatted(.dateTime.year())) + .onSurfacePrimaryForeground() + .headline(.semibold) + } + } + .padding(.small) + .hLeading() + .background { + RoundedRectangle(cornerRadius: .large, style: .continuous) + .fillSurfaceSecondary() } } - .padding(.small) - .hLeading() - .background { - RoundedRectangle(cornerRadius: .large, style: .continuous) - .fillSurfaceSecondary() - } - } - .buttonStyle(.scale) - - Button { - focusedField = nil - viewModel.present(.endTime) - } label: { - VStack(alignment: .leading, spacing: .xxxSmall) { - Text("Ended") - .onSurfaceMediumEmphasisForegroundColor() - .subheadline(.semibold) + .buttonStyle(.scale) - Text(endDateText) - .onSurfaceHighEmphasisForegroundColor() - .headline(.semibold) - - if !isCurrentYearEvent { - Text(viewModel.dateEnd.formatted(.dateTime.year())) - .onSurfaceHighEmphasisForegroundColor() + Button { + focusedField = nil + viewModel.present(.endTime) + } label: { + VStack(alignment: .leading, spacing: .xxxSmall) { + Text("Ended") + .onSurfaceSecondaryForeground() + .subheadline(.semibold) + + Text(endDateText) + .onSurfacePrimaryForeground() .headline(.semibold) + + if !isCurrentYearEvent { + Text(viewModel.dateEnd.formatted(.dateTime.year())) + .onSurfacePrimaryForeground() + .headline(.semibold) + } + } + .padding(.small) + .hLeading() + .background { + RoundedRectangle(cornerRadius: .large, style: .continuous) + .fillSurfaceSecondary() } } - .padding(.small) - .hLeading() - .background { - RoundedRectangle(cornerRadius: .large, style: .continuous) - .fillSurfaceSecondary() - } + .buttonStyle(.scale) } - .buttonStyle(.scale) } - } - var isCurrentYearEvent: Bool { - Calendar.current.component(.year, from: viewModel.dateStart) == Calendar.current.component(.year, from: Date()) && Calendar.current.component(.year, from: viewModel.dateEnd) == Calendar.current.component(.year, from: Date()) - } + var isCurrentYearEvent: Bool { + Calendar.current.component(.year, from: viewModel.dateStart) == Calendar.current.component(.year, from: Date()) && Calendar.current.component(.year, from: viewModel.dateEnd) == Calendar.current.component(.year, from: Date()) + } - var startDateText: String { - if Calendar.current.isDateInToday(viewModel.dateStart) { - return "Today \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" - } else if Calendar.current.isDateInTomorrow(viewModel.dateStart) { - return "Tomorrow \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" - } else if Calendar.current.isDateInYesterday(viewModel.dateStart) { - return "Yesterday \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" - } else { - return "\(viewModel.dateStart.formatted(.dateTime.day().month())) \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" + var startDateText: String { + if Calendar.current.isDateInToday(viewModel.dateStart) { + return "Today \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" + } else if Calendar.current.isDateInTomorrow(viewModel.dateStart) { + return "Tomorrow \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" + } else if Calendar.current.isDateInYesterday(viewModel.dateStart) { + return "Yesterday \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" + } else { + return "\(viewModel.dateStart.formatted(.dateTime.day().month())) \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" + } } - } - var endDateText: String { - if Calendar.current.isDateInToday(viewModel.dateEnd) { - return "Today \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" - } else if Calendar.current.isDateInTomorrow(viewModel.dateEnd) { - return "Tomorrow \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" - } else if Calendar.current.isDateInYesterday(viewModel.dateEnd) { - return "Yesterday \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" - } else { - return "\(viewModel.dateEnd.formatted(.dateTime.day().month())) \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" + var endDateText: String { + if Calendar.current.isDateInToday(viewModel.dateEnd) { + return "Today \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" + } else if Calendar.current.isDateInTomorrow(viewModel.dateEnd) { + return "Tomorrow \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" + } else if Calendar.current.isDateInYesterday(viewModel.dateEnd) { + return "Yesterday \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" + } else { + return "\(viewModel.dateEnd.formatted(.dateTime.day().month())) \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" + } } - } - var bottomBar: some View { - HStack(spacing: .medium) { - Button { - Task { - focusedField = nil - viewModel.present(.location) - } - } label: { - if viewModel.isFetchUpdatePositon { - ProgressView() - } else { - IconDeprecated(.mapPin) -// Icon.Solid.NavigationandTravel.location -// .renderingMode(.template) + var bottomBar: some View { + HStack(spacing: .medium) { + Button { + Task { + focusedField = nil + viewModel.present(.location) + } + } label: { + if viewModel.isFetchUpdatePositon { + ProgressView() + } else { + IconDeprecated(.mapPin) + } } - } - .disabled(viewModel.isFetchUpdatePositon) + .disabled(viewModel.isFetchUpdatePositon) - Button { viewModel.present(.alarm) } label: { - IconDeprecated(.bell) -// Icon.Solid.UserInterface.bell -// .renderingMode(.template) - } + Button { viewModel.present(.alarm) } label: { + IconDeprecated(.bell) + } - Button { viewModel.present(.repeat) } label: { - IconDeprecated(.refresh) - } + Button { viewModel.present(.repeat) } label: { + IconDeprecated(.refresh) + } - /* - Button { viewModel.present(.attachment) } label: { - IconDeprecated(.moreHorizontal) - } - */ + /* + Button { viewModel.present(.attachment) } label: { + IconDeprecated(.moreHorizontal) + } + */ - Spacer() + Spacer() - Button { viewModel.present(.invites) } label: { - IconDeprecated(.userPlus) - } + Button { viewModel.present(.invites) } label: { + IconDeprecated(.userPlus) + } // Icon.Solid.UserInterface.plusCrFr // .renderingMode(.template) + } + .buttonStyle(.scale) + .padding(.horizontal, .medium) + .padding(.vertical, 20) + .onSurfaceSecondaryForeground() + #if !os(watchOS) + .background(.ultraThinMaterial) + #endif + .overlay(alignment: .top) { + Rectangle() + .fill(Color.onSurfacePrimary.opacity(0.05)) + .frame(height: 1) + } } - .buttonStyle(.scale) - .padding(.horizontal, .medium) - .padding(.vertical, 20) - .onSurfaceMediumEmphasisForegroundColor() - .background(.ultraThinMaterial) - .overlay(alignment: .top) { - Rectangle() - .fill(Color.onSurfaceHighEmphasis.opacity(0.05)) - .frame(height: 1) - } - } - @ViewBuilder - private func placeholder() -> some View {} -} + @ViewBuilder + private func placeholder() -> some View {} + } -extension CreateEventView { - enum FocusField: Hashable { - case title - case note - case url + extension CreateEventView { + enum FocusField: Hashable { + case title + case note + case url + } } -} -struct CreateEventView_Previews: PreviewProvider { - static var previews: some View { - CreateEventView() + struct CreateEventView_Previews: PreviewProvider { + static var previews: some View { + CreateEventView() + } } -} +#endif diff --git a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewModel.swift b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewModel.swift index a7f3335..004698e 100644 --- a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewModel.swift +++ b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewModel.swift @@ -3,7 +3,9 @@ // CreateEventViewModel.swift // -import EventKit +#if canImport(EventKit) + @preconcurrency import EventKit +#endif import Factory import OversizeCalendarService import OversizeCore @@ -11,173 +13,174 @@ import OversizeLocationService import OversizeModels import SwiftUI -public enum CreateEventType: Equatable { - case new(Date?, calendar: EKCalendar?) - case update(EKEvent) -} - -@MainActor -public class CreateEventViewModel: ObservableObject { - @Injected(\.calendarService) private var calendarService: CalendarService - @Injected(\.locationService) private var locationService: LocationServiceProtocol - - @Published var state = CreateEventViewModelState.initial - @Published var sheet: CreateEventViewModel.Sheet? = nil - @Published var isFetchUpdatePositon: Bool = .init(false) - - @Published var title: String = .init() - @Published var note: String = .init() - @Published var url: String = .init() - @Published var dateStart: Date = .init() - @Published var dateEnd: Date = .init().halfHour - @Published var isAllDay: Bool = .init(false) - @Published var calendar: EKCalendar? - @Published var calendars: [EKCalendar] = .init() - @Published var sourses: [EKSource] = .init() - @Published var locationName: String? - @Published var location: CLLocationCoordinate2D? - @Published var repitRule: CalendarEventRecurrenceRules = .never - @Published var repitEndRule: CalendarEventEndRecurrenceRules = .never - @Published var alarms: [CalendarAlertsTimes] = .init() - @Published var members: [String] = .init() - @Published var span: EKSpan? - - let type: CreateEventType - - var isLocationSelected: Bool { - location != nil +#if !os(tvOS) + public enum CreateEventType: Equatable, @unchecked Sendable { + case new(Date?, calendar: EKCalendar?) + case update(EKEvent) } - public init(_ type: CreateEventType) { - self.type = type - setEvent(type: type) - } + public class CreateEventViewModel: ObservableObject, @unchecked Sendable { + @Injected(\.calendarService) private var calendarService: CalendarService + @Injected(\.locationService) private var locationService: LocationServiceProtocol + + @Published var state = CreateEventViewModelState.initial + @Published var sheet: CreateEventViewModel.Sheet? = nil + @Published var isFetchUpdatePositon: Bool = .init(false) + + @Published var title: String = .init() + @Published var note: String = .init() + @Published var url: String = .init() + @Published var dateStart: Date = .init() + @Published var dateEnd: Date = .init().halfHour + @Published var isAllDay: Bool = .init(false) + @Published var calendar: EKCalendar? + @Published var calendars: [EKCalendar] = .init() + @Published var sourses: [EKSource] = .init() + @Published var locationName: String? + @Published var location: CLLocationCoordinate2D? + @Published var repitRule: CalendarEventRecurrenceRules = .never + @Published var repitEndRule: CalendarEventEndRecurrenceRules = .never + @Published var alarms: [CalendarAlertsTimes] = .init() + @Published var members: [String] = .init() + @Published var span: EKSpan? + + let type: CreateEventType + + var isLocationSelected: Bool { + location != nil + } - func setEvent(type: CreateEventType) { - switch type { - case let .new(date, calendar): - if let date { - dateStart = date - dateEnd = date.halfHour - } - if let calendar { - self.calendar = calendar - } - case let .update(event): - title = event.title - note = event.notes ?? "" - url = event.url?.absoluteString ?? "" - dateStart = event.startDate - dateEnd = event.endDate - isAllDay = event.isAllDay - calendar = event.calendar - locationName = event.location - if let coordinate = event.structuredLocation?.geoLocation?.coordinate { - location = coordinate + public init(_ type: CreateEventType) { + self.type = type + setEvent(type: type) + } + + func setEvent(type: CreateEventType) { + switch type { + case let .new(date, calendar): + if let date { + dateStart = date + dateEnd = date.halfHour + } + if let calendar { + self.calendar = calendar + } + case let .update(event): + title = event.title + note = event.notes ?? "" + url = event.url?.absoluteString ?? "" + dateStart = event.startDate + dateEnd = event.endDate + isAllDay = event.isAllDay + calendar = event.calendar + locationName = event.location + if let coordinate = event.structuredLocation?.geoLocation?.coordinate { + location = coordinate + } + if let rule = event.recurrenceRules?.first { + repitRule = rule.calendarRecurrenceRule + repitEndRule = rule.recurrenceEnd?.calendarEndRecurrenceRule ?? .never + } + if let eventAlarms = event.alarms { + alarms = eventAlarms.compactMap { $0.calendarAlert } + } + if let attendees = event.attendees { + members = attendees.compactMap { $0.url.absoluteString } + } } - if let rule = event.recurrenceRules?.first { - repitRule = rule.calendarRecurrenceRule - repitEndRule = rule.recurrenceEnd?.calendarEndRecurrenceRule ?? .never + } + + func fetchData() async { + state = .loading + async let calendarsResult = await calendarService.fetchCalendars() + switch await calendarsResult { + case let .success(data): + log("✅ EKCalendars fetched") + calendars = data + case let .failure(error): + log("❌ EKCalendars not fetched (\(error.title))") + state = .error(error) } - if let eventAlarms = event.alarms { - alarms = eventAlarms.compactMap { $0.calendarAlert } + async let soursesResult = await calendarService.fetchSourses() + switch await soursesResult { + case let .success(data): + log("✅ EKSource fetched") + sourses = data + case let .failure(error): + log("❌ EKSource not fetched (\(error.title))") + state = .error(error) } - if let attendees = event.attendees { - members = attendees.compactMap { $0.url.absoluteString } + if case let .new(_, calendar) = type, calendar == nil { + let result = await calendarService.fetchDefaultCalendar() + switch result { + case let .success(calendar): + self.calendar = calendar + case let .failure(error): + log("❌ Default calendar not fetched (\(error.title))") + } } } - } - func fetchData() async { - state = .loading - async let calendarsResult = await calendarService.fetchCalendars() - switch await calendarsResult { - case let .success(data): - log("✅ EKCalendars fetched") - calendars = data - case let .failure(error): - log("❌ EKCalendars not fetched (\(error.title))") - state = .error(error) - } - async let soursesResult = await calendarService.fetchSourses() - switch await soursesResult { - case let .success(data): - log("✅ EKSource fetched") - sourses = data - case let .failure(error): - log("❌ EKSource not fetched (\(error.title))") - state = .error(error) - } - if case let .new(_, calendar) = type, calendar == nil { - let result = await calendarService.fetchDefaultCalendar() + func save() async -> Result { + var oldEvent: EKEvent? + + if case let .update(event) = type { + oldEvent = event + } + + let result = await calendarService.createEvent( + event: oldEvent, + title: title, + notes: note, + startDate: dateStart, + endDate: dateEnd, + calendar: calendar, + isAllDay: isAllDay, + location: locationName, + structuredLocation: getEKStructuredLocation(), + alarms: alarms, + url: URL(string: url), + memberEmails: members, + recurrenceRules: repitRule, + recurrenceEndRules: repitEndRule, + span: span ?? .thisEvent + ) switch result { - case let .success(calendar): - self.calendar = calendar + case let .success(data): + log("✅ EKEvent saved") + return .success(data) case let .failure(error): - log("❌ Default calendar not fetched (\(error.title))") + log("❌ EKEvent not saved (\(error.title))") + return .failure(error) } } - } - func save() async -> Result { - var oldEvent: EKEvent? - - if case let .update(event) = type { - oldEvent = event - } - - let result = await calendarService.createEvent( - event: oldEvent, - title: title, - notes: note, - startDate: dateStart, - endDate: dateEnd, - calendar: calendar, - isAllDay: isAllDay, - location: locationName, - structuredLocation: getEKStructuredLocation(), - alarms: alarms, - url: URL(string: url), - memberEmails: members, - recurrenceRules: repitRule, - recurrenceEndRules: repitEndRule, - span: span ?? .thisEvent - ) - switch result { - case let .success(data): - log("✅ EKEvent saved") - return .success(data) - case let .failure(error): - log("❌ EKEvent not saved (\(error.title))") - return .failure(error) + func getEKStructuredLocation() -> EKStructuredLocation? { + if let location { + let structuredLocation: EKStructuredLocation? + let location = CLLocation(latitude: location.latitude, longitude: location.longitude) + structuredLocation = EKStructuredLocation(title: locationName ?? "") // same title with ekEvent.location + structuredLocation?.geoLocation = location + return structuredLocation + } else { + return nil + } } - } - func getEKStructuredLocation() -> EKStructuredLocation? { - if let location { - let structuredLocation: EKStructuredLocation? - let location = CLLocation(latitude: location.latitude, longitude: location.longitude) - structuredLocation = EKStructuredLocation(title: locationName ?? "") // same title with ekEvent.location - structuredLocation?.geoLocation = location - return structuredLocation - } else { - return nil + func updateCurrentPosition() async throws { + isFetchUpdatePositon = true + let currentPosition = try await locationService.currentLocation() + guard let newLocation = currentPosition else { return } + location = newLocation + log("📍 Location: \(newLocation.latitude), \(newLocation.longitude)") + isFetchUpdatePositon = false } } - func updateCurrentPosition() async throws { - isFetchUpdatePositon = true - let currentPosition = try await locationService.currentLocation() - guard let newLocation = currentPosition else { return } - location = newLocation - log("📍 Location: \(newLocation.latitude), \(newLocation.longitude)") - isFetchUpdatePositon = false + public enum CreateEventViewModelState { + case initial + case loading + case result([EKEvent]) + case error(AppError) } -} - -public enum CreateEventViewModelState { - case initial - case loading - case result([EKEvent]) - case error(AppError) -} +#endif diff --git a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewSheet.swift b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewSheet.swift index 5136669..beb178c 100644 --- a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewSheet.swift +++ b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewSheet.swift @@ -3,112 +3,119 @@ // CreateEventViewSheet.swift // -import EventKit +#if canImport(EventKit) + import EventKit +#endif import OversizeComponents import OversizeContactsKit import OversizeLocationKit import OversizeUI import SwiftUI -public extension CreateEventViewModel { - func present(_ sheet: CreateEventViewModel.Sheet) { - self.sheet = sheet - } +#if !os(tvOS) + public extension CreateEventViewModel { + func present(_ sheet: CreateEventViewModel.Sheet) { + self.sheet = sheet + } - func close() { - sheet = nil + func close() { + sheet = nil + } } -} -public extension CreateEventViewModel { - enum Sheet { - case startTime - case endTime - case attachment - case calendar - case location - case `repeat` - case alarm - case invites - case span - } // attachment, alert, invitees -} - -extension CreateEventViewModel.Sheet: Identifiable { - public var id: String { - switch self { - case .startTime: - return "startTime" - case .endTime: - return "endTime" - case .attachment: - return "attachment" - case .calendar: - return "calendar" - case .location: - return "location" - case .repeat: - return "repeat" - case .alarm: - return "alarm" - case .invites: - return "alarm" - case .span: - return "span" - } + public extension CreateEventViewModel { + enum Sheet { + case startTime + case endTime + case attachment + case calendar + case location + case `repeat` + case alarm + case invites + case span + } // attachment, alert, invitees } -} -public extension CreateEventView { - func resolveSheet(sheet: CreateEventViewModel.Sheet) -> some View { - Group { - switch sheet { + extension CreateEventViewModel.Sheet: Identifiable { + public var id: String { + switch self { case .startTime: - #if os(iOS) - DatePickerSheet(title: "Starts time", selection: $viewModel.dateStart) - .onDisappear { - if viewModel.dateStart > viewModel.dateEnd { - viewModel.dateEnd = viewModel.dateStart.halfHour - } - } - .presentationDetents([.height(500)]) - #else - EmptyView() - #endif + return "startTime" case .endTime: - #if os(iOS) - DatePickerSheet(title: "Ends time", selection: $viewModel.dateEnd) - .datePickerMinimumDate(viewModel.dateStart.minute) - .presentationDetents([.height(500)]) - #else - EmptyView() - #endif + return "endTime" case .attachment: - AttachmentView() - .presentationDetents([.height(270)]) + return "attachment" case .calendar: - CalendarPicker(selection: $viewModel.calendar, calendars: viewModel.calendars, sourses: viewModel.sourses) - .presentationDetents([.large]) + return "calendar" case .location: - AddressPicker(address: $viewModel.locationName, location: $viewModel.location) - .interactiveDismissDisabled(true) - .presentationDetents([.large]) + return "location" case .repeat: - RepeatPicker(selectionRule: $viewModel.repitRule, selectionEndRule: $viewModel.repitEndRule) - + return "repeat" case .alarm: - AlarmPicker(selection: $viewModel.alarms) - .presentationDetents([.height(630), .large]) - .presentationDragIndicator(.hidden) + return "alarm" case .invites: - EmailPickerView(selection: $viewModel.members) - .presentationDetents([.large]) - .interactiveDismissDisabled(true) + return "alarm" case .span: - SaveForView(selection: $viewModel.span) - .presentationDetents([.height(270)]) + return "span" + } + } + } + + public extension CreateEventView { + func resolveSheet(sheet: CreateEventViewModel.Sheet) -> some View { + Group { + switch sheet { + case .startTime: + #if os(iOS) + DatePickerSheet(title: "Starts time", selection: $viewModel.dateStart) + .onDisappear { + if viewModel.dateStart > viewModel.dateEnd { + viewModel.dateEnd = viewModel.dateStart.halfHour + } + } + .presentationDetents([.height(500)]) + #else + EmptyView() + #endif + case .endTime: + #if os(iOS) + DatePickerSheet(title: "Ends time", selection: $viewModel.dateEnd) + .datePickerMinimumDate(viewModel.dateStart.minute) + .presentationDetents([.height(500)]) + #else + EmptyView() + #endif + case .attachment: + AttachmentView() + .presentationDetents([.height(270)]) + case .calendar: + CalendarPicker(selection: $viewModel.calendar, calendars: viewModel.calendars, sourses: viewModel.sourses) + .presentationDetents([.large]) + case .location: + #if !os(watchOS) + AddressPicker(address: $viewModel.locationName, location: $viewModel.location) + .interactiveDismissDisabled(true) + .presentationDetents([.large]) + #else + EmptyView() + #endif + case .repeat: + RepeatPicker(selectionRule: $viewModel.repitRule, selectionEndRule: $viewModel.repitEndRule) + case .alarm: + AlarmPicker(selection: $viewModel.alarms) + .presentationDetents([.height(630), .large]) + .presentationDragIndicator(.hidden) + case .invites: + EmailPickerView(selection: $viewModel.members) + .presentationDetents([.large]) + .interactiveDismissDisabled(true) + case .span: + SaveForView(selection: $viewModel.span) + .presentationDetents([.height(270)]) + } } + .systemServices() } - .systemServices() } -} +#endif diff --git a/Sources/OversizeCalendarKit/CreateEventScreen/SaveForView/SaveForView.swift b/Sources/OversizeCalendarKit/CreateEventScreen/SaveForView/SaveForView.swift index b8ee9d7..676b9a5 100644 --- a/Sources/OversizeCalendarKit/CreateEventScreen/SaveForView/SaveForView.swift +++ b/Sources/OversizeCalendarKit/CreateEventScreen/SaveForView/SaveForView.swift @@ -3,46 +3,50 @@ // SaveForView.swift // -import EventKit +#if canImport(EventKit) + import EventKit +#endif import OversizeUI import SwiftUI -public struct SaveForView: View { - @Environment(\.dismiss) var dismiss - @Binding private var span: EKSpan? +#if !os(tvOS) + public struct SaveForView: View { + @Environment(\.dismiss) var dismiss + @Binding private var span: EKSpan? - public init(selection: Binding) { - _span = selection - } + public init(selection: Binding) { + _span = selection + } - public var body: some View { - PageView("This is repeating event") { - SectionView { - VStack(spacing: .zero) { - Row("Save for this event only") { - span = .thisEvent - dismiss() - } leading: { - Image.Date.calendar - .renderingMode(.template) - .foregroundColor(.onSurfaceHighEmphasis) - } + public var body: some View { + PageView("This is repeating event") { + SectionView { + VStack(spacing: .zero) { + Row("Save for this event only") { + span = .thisEvent + dismiss() + } leading: { + Image.Date.calendar + .renderingMode(.template) + .foregroundColor(.onSurfacePrimary) + } - Row("Save for feature events") { - span = .futureEvents - dismiss() - } leading: { - Image.Base.calendar - .renderingMode(.template) - .foregroundColor(.onSurfaceHighEmphasis) + Row("Save for feature events") { + span = .futureEvents + dismiss() + } leading: { + Image.Base.calendar + .renderingMode(.template) + .foregroundColor(.onSurfacePrimary) + } } } + .surfaceContentRowMargins() + } + .backgroundSecondary() + .leadingBar { + BarButton(.close) } - .surfaceContentRowMargins() - } - .backgroundSecondary() - .leadingBar { - BarButton(.close) } } -} +#endif diff --git a/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift b/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift index 68fbfb8..f31672c 100644 --- a/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift +++ b/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift @@ -3,51 +3,55 @@ // AlertPicker.swift // -import EventKit +#if canImport(EventKit) + import EventKit +#endif import OversizeCalendarService import OversizeUI import SwiftUI -public struct AlarmPicker: View { - @Environment(\.dismiss) var dismiss - @Binding private var selection: [CalendarAlertsTimes] - @State private var selectedAlerts: [CalendarAlertsTimes] = [] +#if !os(tvOS) + public struct AlarmPicker: View { + @Environment(\.dismiss) var dismiss + @Binding private var selection: [CalendarAlertsTimes] + @State private var selectedAlerts: [CalendarAlertsTimes] = [] - public init(selection: Binding<[CalendarAlertsTimes]>) { - _selection = selection - _selectedAlerts = State(wrappedValue: selection.wrappedValue) - } + public init(selection: Binding<[CalendarAlertsTimes]>) { + _selection = selection + _selectedAlerts = State(wrappedValue: selection.wrappedValue) + } - public var body: some View { - PageView("Alarm") { - SectionView { - VStack(spacing: .zero) { - ForEach(CalendarAlertsTimes.allCases) { alert in - Checkbox(alert.title, isOn: .constant((selectedAlerts.first { $0.id == alert.id } != nil) ? true : false)) { - if !selectedAlerts.isEmpty, let _ = selectedAlerts.first(where: { $0.id == alert.id }) { - selectedAlerts.remove(alert) - } else { - selectedAlerts.append(alert) + public var body: some View { + PageView("Alarm") { + SectionView { + VStack(spacing: .zero) { + ForEach(CalendarAlertsTimes.allCases) { alert in + Checkbox(alert.title, isOn: .constant((selectedAlerts.first { $0.id == alert.id } != nil) ? true : false)) { + if !selectedAlerts.isEmpty, let _ = selectedAlerts.first(where: { $0.id == alert.id }) { + selectedAlerts.remove(alert) + } else { + selectedAlerts.append(alert) + } } } } } + .surfaceContentRowMargins() + } + .backgroundSecondary() + .leadingBar { + BarButton(.close) + } + .trailingBar { + BarButton(.accent("Done", action: { + selection = selectedAlerts + dismiss() + })) + .disabled(selectedAlerts.isEmpty) } - .surfaceContentRowMargins() - } - .backgroundSecondary() - .leadingBar { - BarButton(.close) - } - .trailingBar { - BarButton(.accent("Done", action: { - selection = selectedAlerts - dismiss() - })) - .disabled(selectedAlerts.isEmpty) } } -} +#endif // struct AlertPicker_Previews: PreviewProvider { // static var previews: some View { diff --git a/Sources/OversizeCalendarKit/Pickers/CalendarPicker.swift b/Sources/OversizeCalendarKit/Pickers/CalendarPicker.swift index 173685e..8244d84 100644 --- a/Sources/OversizeCalendarKit/Pickers/CalendarPicker.swift +++ b/Sources/OversizeCalendarKit/Pickers/CalendarPicker.swift @@ -3,66 +3,70 @@ // CalendarPicker.swift // -import EventKit +#if canImport(EventKit) + import EventKit +#endif import OversizeUI import SwiftUI -public struct CalendarPicker: View { - @Environment(\.dismiss) var dismiss +#if !os(tvOS) + public struct CalendarPicker: View { + @Environment(\.dismiss) var dismiss - @Binding private var selection: EKCalendar? + @Binding private var selection: EKCalendar? - private let calendars: [EKCalendar] + private let calendars: [EKCalendar] - private let sourses: [EKSource] + private let sourses: [EKSource] - private let closable: Bool + private let closable: Bool - public init(selection: Binding, calendars: [EKCalendar], sourses: [EKSource], closable: Bool = true) { - _selection = selection - self.calendars = calendars - self.sourses = sourses - self.closable = closable - } + public init(selection: Binding, calendars: [EKCalendar], sourses: [EKSource], closable: Bool = true) { + _selection = selection + self.calendars = calendars + self.sourses = sourses + self.closable = closable + } - public var body: some View { - PageView("Calendar") { - ForEach(sourses, id: \.sourceIdentifier) { source in - let filtredCalendar: [EKCalendar] = calendars.filter { $0.source.sourceIdentifier == source.sourceIdentifier && $0.allowsContentModifications } - if !filtredCalendar.isEmpty { - calendarSection(source: source, calendars: filtredCalendar) + public var body: some View { + PageView("Calendar") { + ForEach(sourses, id: \.sourceIdentifier) { source in + let filtredCalendar: [EKCalendar] = calendars.filter { $0.source.sourceIdentifier == source.sourceIdentifier && $0.allowsContentModifications } + if !filtredCalendar.isEmpty { + calendarSection(source: source, calendars: filtredCalendar) + } } } + .backgroundSecondary() + .leadingBar { + BarButton(closable ? .close : .back) + } } - .backgroundSecondary() - .leadingBar { - BarButton(closable ? .close : .back) - } - } - func calendarSection(source: EKSource, calendars: [EKCalendar]) -> some View { - SectionView(source.title) { - VStack(spacing: .zero) { - ForEach(calendars, id: \.calendarIdentifier) { calendar in - Radio(isOn: selection?.calendarIdentifier == calendar.calendarIdentifier) { - selection = calendar - dismiss() - } label: { - Row(calendar.title) { - Circle() - .fill(Color(calendar.cgColor)) - .frame(width: 16, height: 16) + func calendarSection(source: EKSource, calendars: [EKCalendar]) -> some View { + SectionView(source.title) { + VStack(spacing: .zero) { + ForEach(calendars, id: \.calendarIdentifier) { calendar in + Radio(isOn: selection?.calendarIdentifier == calendar.calendarIdentifier) { + selection = calendar + dismiss() + } label: { + Row(calendar.title) { + Circle() + .fill(Color(calendar.cgColor)) + .frame(width: 16, height: 16) + } } } } } + .surfaceContentRowMargins() } - .surfaceContentRowMargins() } -} -struct CalendarPicker_Previews: PreviewProvider { - static var previews: some View { - CalendarPicker(selection: .constant(nil), calendars: [], sourses: []) + struct CalendarPicker_Previews: PreviewProvider { + static var previews: some View { + CalendarPicker(selection: .constant(nil), calendars: [], sourses: []) + } } -} +#endif diff --git a/Sources/OversizeCalendarKit/Pickers/RepeatPicker.swift b/Sources/OversizeCalendarKit/Pickers/RepeatPicker.swift index ec7b5d2..2c32cb8 100644 --- a/Sources/OversizeCalendarKit/Pickers/RepeatPicker.swift +++ b/Sources/OversizeCalendarKit/Pickers/RepeatPicker.swift @@ -3,132 +3,134 @@ // RepeatPicker.swift // -import EventKit +#if canImport(EventKit) + import EventKit +#endif import OversizeCalendarService import OversizeUI import SwiftUI -public struct RepeatPicker: View { - @Environment(\.dismiss) private var dismiss +#if !os(tvOS) + public struct RepeatPicker: View { + @Environment(\.dismiss) private var dismiss - @Binding private var selectionRule: CalendarEventRecurrenceRules - @Binding private var selectionEndRule: CalendarEventEndRecurrenceRules + @Binding private var selectionRule: CalendarEventRecurrenceRules + @Binding private var selectionEndRule: CalendarEventEndRecurrenceRules - @State private var rule: CalendarEventRecurrenceRules - @State private var endRule: CalendarEventEndRecurrenceRules + @State private var rule: CalendarEventRecurrenceRules + @State private var endRule: CalendarEventEndRecurrenceRules - @State private var endDate: Date = .init() - @State private var repeatCount: String = "1" - @FocusState private var isFocusedRepitCount: Bool + @State private var endDate: Date = .init() + @State private var repeatCount: String = "1" + @FocusState private var isFocusedRepitCount: Bool - public init(selectionRule: Binding, selectionEndRule: Binding) { - _selectionRule = selectionRule - _selectionEndRule = selectionEndRule - _rule = State(wrappedValue: selectionRule.wrappedValue) - _endRule = State(wrappedValue: selectionEndRule.wrappedValue) - } + public init(selectionRule: Binding, selectionEndRule: Binding) { + _selectionRule = selectionRule + _selectionEndRule = selectionEndRule + _rule = State(wrappedValue: selectionRule.wrappedValue) + _endRule = State(wrappedValue: selectionEndRule.wrappedValue) + } - public var body: some View { - ScrollViewReader { scrollView in - PageView("Repeat") { - SectionView { - VStack(spacing: .zero) { - ForEach(CalendarEventRecurrenceRules.allCases) { rule in - Radio(isOn: self.rule.id == rule.id) { - withAnimation { - self.rule = rule + public var body: some View { + ScrollViewReader { scrollView in + PageView("Repeat") { + SectionView { + VStack(spacing: .zero) { + ForEach(CalendarEventRecurrenceRules.allCases) { rule in + Radio(isOn: self.rule.id == rule.id) { + withAnimation { + self.rule = rule + } + } label: { + Row(rule.title) } - } label: { - Row(rule.title) } } } - } - if rule != .never { - SectionView("End Repeat") { - VStack(spacing: .zero) { - ForEach(CalendarEventEndRecurrenceRules.allCases) { rule in - VStack(spacing: .xxSmall) { - Radio(isOn: endRule.id == rule.id) { - endRule = rule - if case .occurrenceCount = endRule { - isFocusedRepitCount = true - scrollView.scrollTo(rule.id) - } + if rule != .never { + SectionView("End Repeat") { + VStack(spacing: .zero) { + ForEach(CalendarEventEndRecurrenceRules.allCases) { rule in + VStack(spacing: .xxSmall) { + Radio(isOn: endRule.id == rule.id) { + endRule = rule + if case .occurrenceCount = endRule { + isFocusedRepitCount = true + scrollView.scrollTo(rule.id) + } - if case .endDate = endRule { - isFocusedRepitCount = true - scrollView.scrollTo(rule.id) + if case .endDate = endRule { + isFocusedRepitCount = true + scrollView.scrollTo(rule.id) + } + } label: { + Row(rule.title) } - } label: { - Row(rule.title) - } - if endRule.id == rule.id { - repartPicker(rules: rule) - .padding(.horizontal, .medium) - .padding(.bottom, .small) + if endRule.id == rule.id { + repartPicker(rules: rule) + .padding(.horizontal, .medium) + .padding(.bottom, .small) + } } } } } + .transition(.move(edge: .top)) } - .transition(.move(edge: .top)) } + .backgroundSecondary() + .leadingBar { + BarButton(.close) + } + .trailingBar { + BarButton(.accent("Done", action: { + selectionRule = rule + selectionEndRule = endRule + dismiss() + })) + .disabled(rule == .never) + } + .surfaceContentRowMargins() } - .backgroundSecondary() - .leadingBar { - BarButton(.close) - } - .trailingBar { - BarButton(.accent("Done", action: { - selectionRule = rule - selectionEndRule = endRule - dismiss() - })) - .disabled(rule == .never) - } - .surfaceContentRowMargins() + .presentationDetents(rule == .never ? [.height(630), .large] : [.large]) + .presentationDragIndicator(.hidden) } - .presentationDetents(rule == .never ? [.height(630), .large] : [.large]) - .presentationDragIndicator(.hidden) - } - @ViewBuilder - func repartPicker(rules: CalendarEventEndRecurrenceRules) -> some View { - switch rules { - case .never: - EmptyView() - case .occurrenceCount: - TextField("Number of repetitions", text: Binding(get: { - repeatCount - }, set: { newValue in - repeatCount = newValue - endRule = .occurrenceCount(Int(newValue) ?? 1) - })) - #if os(iOS) - .keyboardType(.numberPad) - #endif - .textFieldStyle(DefaultPlaceholderTextFieldStyle()) - .focused($isFocusedRepitCount) - case .endDate: - DatePicker("Date", selection: Binding(get: { - endDate - }, set: { newDate in - endDate = newDate - endRule = .endDate(newDate) - })) - #if os(iOS) - .datePickerStyle(.wheel) - #endif - .labelsHidden() + @ViewBuilder + func repartPicker(rules: CalendarEventEndRecurrenceRules) -> some View { + switch rules { + case .never: + EmptyView() + case .occurrenceCount: + TextField("Number of repetitions", text: Binding(get: { + repeatCount + }, set: { newValue in + repeatCount = newValue + endRule = .occurrenceCount(Int(newValue) ?? 1) + })) + #if os(iOS) + .keyboardType(.numberPad) + #endif + .textFieldStyle(.default) + .focused($isFocusedRepitCount) + case .endDate: + #if !os(watchOS) + DatePicker("Date", selection: Binding(get: { + endDate + }, set: { newDate in + endDate = newDate + endRule = .endDate(newDate) + })) + #if os(iOS) + .datePickerStyle(.wheel) + #endif + .labelsHidden() + #else + ProgressView() + #endif + } } } -} - -// struct RepeatPicker_Previews: PreviewProvider { -// static var previews: some View { -// RepeatPicker() -// } -// } +#endif diff --git a/Sources/OversizeContactsKit/AttendeesList/AttendeesView.swift b/Sources/OversizeContactsKit/AttendeesList/AttendeesView.swift index 3125879..3bca2d1 100644 --- a/Sources/OversizeContactsKit/AttendeesList/AttendeesView.swift +++ b/Sources/OversizeContactsKit/AttendeesList/AttendeesView.swift @@ -3,8 +3,10 @@ // AttendeesView.swift // -import Contacts -import EventKit +#if canImport(Contacts) && canImport(EventKit) + import Contacts + import EventKit +#endif import OversizeCalendarService import OversizeContactsService import OversizeCore @@ -13,81 +15,87 @@ import OversizeLocalizable import OversizeUI import SwiftUI -public struct AttendeesView: View { - @StateObject var viewModel: AttendeesViewModel - @Environment(\.dismiss) var dismiss +#if !os(tvOS) + public struct AttendeesView: View { + @StateObject var viewModel: AttendeesViewModel + @Environment(\.dismiss) var dismiss - public init(event: EKEvent) { - _viewModel = StateObject(wrappedValue: AttendeesViewModel(event: event)) - } + public init(event: EKEvent) { + _viewModel = StateObject(wrappedValue: AttendeesViewModel(event: event)) + } - public var body: some View { - PageView("Invitees") { - Group { - switch viewModel.state { - case .initial: - placeholder() - .onAppear { - Task { - await viewModel.fetchData() + public var body: some View { + PageView("Invitees") { + Group { + switch viewModel.state { + case .initial: + placeholder() + .onAppear { + Task { + await viewModel.fetchData() + } } - } - case .loading: - placeholder() - case let .result(data): - content(data) - case let .error(error): - ErrorView(error) + case .loading: + placeholder() + case let .result(data): + content(data) + case let .error(error): + ErrorView(error) + } } } + .leadingBar { + BarButton(.close) + } } - .leadingBar { - BarButton(.close) - } - } - @ViewBuilder - private func content(_: [CNContact]) -> some View { - if let attendees = viewModel.event.attendees { - VStack(spacing: .zero) { - if let organizer = viewModel.event.organizer { - Row(organizer.name ?? organizer.url.absoluteString, subtitle: "Organizer") { - userAvatarView(participant: organizer) + @ViewBuilder + private func content(_: [CNContact]) -> some View { + if let attendees = viewModel.event.attendees { + VStack(spacing: .zero) { + if let organizer = viewModel.event.organizer { + Row(organizer.name ?? organizer.url.absoluteString, subtitle: "Organizer") { + userAvatarView(participant: organizer) + } } - } - ForEach(attendees, id: \.self) { attender in - Row(attender.name ?? attender.url.absoluteString, subtitle: attender.participantRole.title) { - userAvatarView(participant: attender) + ForEach(attendees, id: \.self) { attender in + Row(attender.name ?? attender.url.absoluteString, subtitle: attender.participantRole.title) { + userAvatarView(participant: attender) + } } } } } - } - func userAvatarView(participant: EKParticipant) -> some View { - ZStack(alignment: .bottomTrailing) { - Avatar(firstName: participant.name ?? participant.url.absoluteString) - .controlSize(.regular) + func userAvatarView(participant: EKParticipant) -> some View { + ZStack(alignment: .bottomTrailing) { + Avatar(firstName: participant.name ?? participant.url.absoluteString) + .controlSize(.regular) - ZStack { - Circle() - .fill(participant.color) - .frame(width: 16, height: 16) - .background { - Circle() - .stroke(lineWidth: 4) - .fillBackgroundPrimary() - } - Image(systemName: participant.symbolName) - .onPrimaryHighEmphasisForegroundColor() - .font(.system(size: 9, weight: .black)) + ZStack { + Circle() + .fill(participant.color) + .frame(width: 16, height: 16) + .background { + Circle() + .stroke(lineWidth: 4) + .fillBackgroundPrimary() + } + Image(systemName: participant.symbolName) + .onPrimaryForeground() + .font(.system(size: 9, weight: .black)) + } } } - } - @ViewBuilder - private func placeholder() -> some View { - LoaderOverlayView() + @ViewBuilder + private func placeholder() -> some View { + #if os(watchOS) + ProgressView() + #else + LoaderOverlayView() + #endif + } } -} +#endif diff --git a/Sources/OversizeContactsKit/AttendeesList/AttendeesViewModel.swift b/Sources/OversizeContactsKit/AttendeesList/AttendeesViewModel.swift index 3b06806..d597c36 100644 --- a/Sources/OversizeContactsKit/AttendeesList/AttendeesViewModel.swift +++ b/Sources/OversizeContactsKit/AttendeesList/AttendeesViewModel.swift @@ -3,32 +3,35 @@ // AttendeesViewModel.swift // -import Contacts -import EventKit +#if canImport(Contacts) && canImport(EventKit) + @preconcurrency import Contacts + import EventKit +#endif import Factory import OversizeContactsService import OversizeCore import OversizeModels import SwiftUI -@MainActor -class AttendeesViewModel: ObservableObject { - @Injected(\.contactsService) private var contactsService: ContactsService - @Published var state = AttendeesViewModelState.initial - @Published var searchText: String = .init() +#if !os(tvOS) + @MainActor + class AttendeesViewModel: ObservableObject { + @Injected(\.contactsService) private var contactsService: ContactsService + @Published var state = AttendeesViewModelState.initial + @Published var searchText: String = .init() - let event: EKEvent + let event: EKEvent - init(event: EKEvent) { - self.event = event - } + init(event: EKEvent) { + self.event = event + } + + func fetchData() async { + state = .loading + let _ = await contactsService.requestAccess() - func fetchData() async { - state = .loading - let _ = await contactsService.requestAccess() - do { let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactThumbnailImageDataKey] - let result = try await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) + let result = await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) switch result { case let .success(data): log("✅ CNContact fetched") @@ -37,27 +40,25 @@ class AttendeesViewModel: ObservableObject { log("❌ CNContact not fetched (\(error.title))") state = .error(error) } - } catch { - state = .error(.custom(title: "Not contacts")) } - } - func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { - for contact in contacts where !contact.emailAddresses.isEmpty { - for emailAddress in contact.emailAddresses { - let emailAddressString = emailAddress.value as String - if emailAddressString == email { - return contact + func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { + for contact in contacts where !contact.emailAddresses.isEmpty { + for emailAddress in contact.emailAddresses { + let emailAddressString = emailAddress.value as String + if emailAddressString == email { + return contact + } } } + return nil } - return nil } -} - -enum AttendeesViewModelState { - case initial - case loading - case result([CNContact]) - case error(AppError) -} + + enum AttendeesViewModelState { + case initial + case loading + case result([CNContact]) + case error(AppError) + } +#endif diff --git a/Sources/OversizeContactsKit/ContactsLists/ContactsListsView.swift b/Sources/OversizeContactsKit/ContactsLists/ContactsListsView.swift index c1d8c82..11d868c 100644 --- a/Sources/OversizeContactsKit/ContactsLists/ContactsListsView.swift +++ b/Sources/OversizeContactsKit/ContactsLists/ContactsListsView.swift @@ -3,7 +3,9 @@ // ContactsListsView.swift // -import Contacts +#if canImport(Contacts) + import Contacts +#endif import OversizeComponents import OversizeCore import OversizeKit @@ -11,83 +13,85 @@ import OversizeLocalizable import OversizeUI import SwiftUI -public struct ContactsListsView: View { - @StateObject var viewModel: ContactsListsViewModel - @Environment(\.dismiss) var dismiss - @Binding private var emails: [String] +#if !os(tvOS) + public struct ContactsListsView: View { + @StateObject var viewModel: ContactsListsViewModel + @Environment(\.dismiss) var dismiss + @Binding private var emails: [String] - public init(emails: Binding<[String]>) { - _viewModel = StateObject(wrappedValue: ContactsListsViewModel()) - _emails = emails - } + public init(emails: Binding<[String]>) { + _viewModel = StateObject(wrappedValue: ContactsListsViewModel()) + _emails = emails + } - public var body: some View { - PageView("") { - Group { - switch viewModel.state { - case .initial: - placeholder() - case .loading: - placeholder() - case let .result(data): - content(data: data) - case let .error(error): - ErrorView(error) + public var body: some View { + PageView("") { + Group { + switch viewModel.state { + case .initial: + placeholder() + case .loading: + placeholder() + case let .result(data): + content(data: data) + case let .error(error): + ErrorView(error) + } } } + .leadingBar { + BarButton(.close) + } + .task { + await viewModel.fetchData() + } } - .leadingBar { - BarButton(.close) - } - .task { - await viewModel.fetchData() - } - } - @ViewBuilder - private func content(data: [CNContact]) -> some View { - ForEach(emails, id: \.self) { email in - if let contact = viewModel.getContactFromEmail(email: email, contacts: data) { - let emails = contact.emailAddresses - if !emails.isEmpty { - ForEach(emails, id: \.identifier) { email in - emailRow(email: email, contact: contact) + @ViewBuilder + private func content(data: [CNContact]) -> some View { + ForEach(emails, id: \.self) { email in + if let contact = viewModel.getContactFromEmail(email: email, contacts: data) { + let emails = contact.emailAddresses + if !emails.isEmpty { + ForEach(emails, id: \.identifier) { email in + emailRow(email: email, contact: contact) + } + } + } else { + Row(email) { + Avatar(firstName: email) } - } - } else { - Row(email) { - Avatar(firstName: email) } } } - } - @ViewBuilder - private func emailRow(email: CNLabeledValue, contact: CNContact) -> some View { - let email = email.value as String - #if os(iOS) - if let avatarThumbnailData = contact.thumbnailImageData, let avatarThumbnail = UIImage(data: avatarThumbnailData) { - Row(contact.givenName + " " + contact.familyName, subtitle: email) { - Avatar(firstName: contact.givenName, lastName: contact.familyName, avatar: Image(uiImage: avatarThumbnail)) + @ViewBuilder + private func emailRow(email: CNLabeledValue, contact: CNContact) -> some View { + let email = email.value as String + #if os(iOS) + if let avatarThumbnailData = contact.thumbnailImageData, let avatarThumbnail = UIImage(data: avatarThumbnailData) { + Row(contact.givenName + " " + contact.familyName, subtitle: email) { + Avatar(firstName: contact.givenName, lastName: contact.familyName, avatar: Image(uiImage: avatarThumbnail)) + } + } else { + Row(contact.givenName + " " + contact.familyName, subtitle: email) { + Avatar(firstName: contact.givenName, lastName: contact.familyName) + } } - } else { + #else Row(contact.givenName + " " + contact.familyName, subtitle: email) { Avatar(firstName: contact.givenName, lastName: contact.familyName) } - } - #else - Row(contact.givenName + " " + contact.familyName, subtitle: email) { - Avatar(firstName: contact.givenName, lastName: contact.familyName) - } - #endif - } + #endif + } - @ViewBuilder - private func placeholder() -> some View { - ForEach(emails, id: \.self) { email in - Row(email) { - Avatar(firstName: email) + @ViewBuilder + private func placeholder() -> some View { + ForEach(emails, id: \.self) { email in + Row(email) { + Avatar(firstName: email) + } } } } -} +#endif diff --git a/Sources/OversizeContactsKit/ContactsLists/ContactsListsViewModel.swift b/Sources/OversizeContactsKit/ContactsLists/ContactsListsViewModel.swift index bed0d93..a7ce31c 100644 --- a/Sources/OversizeContactsKit/ContactsLists/ContactsListsViewModel.swift +++ b/Sources/OversizeContactsKit/ContactsLists/ContactsListsViewModel.swift @@ -3,27 +3,30 @@ // ContactsListsViewModel.swift // -import Contacts +#if canImport(Contacts) + @preconcurrency import Contacts +#endif import Factory import OversizeContactsService import OversizeCore import OversizeModels import SwiftUI -@MainActor -public class ContactsListsViewModel: ObservableObject { - @Injected(\.contactsService) private var contactsService: ContactsService - @Published var state = ContactsPickerViewModelState.initial - @Published var searchText: String = .init() +#if !os(tvOS) + @MainActor + public class ContactsListsViewModel: ObservableObject { + @Injected(\.contactsService) private var contactsService: ContactsService + @Published var state = ContactsPickerViewModelState.initial + @Published var searchText: String = .init() - public init() {} + public init() {} + + func fetchData() async { + state = .loading + let _ = await contactsService.requestAccess() - func fetchData() async { - state = .loading - let _ = await contactsService.requestAccess() - do { let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactThumbnailImageDataKey] - let result = try await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) + let result = await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) switch result { case let .success(data): log("✅ CNContact fetched") @@ -32,27 +35,25 @@ public class ContactsListsViewModel: ObservableObject { log("❌ CNContact not fetched (\(error.title))") state = .error(error) } - } catch { - state = .error(.custom(title: "Not contacts")) } - } - func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { - for contact in contacts where !contact.emailAddresses.isEmpty { - for emailAddress in contact.emailAddresses { - let emailAddressString = emailAddress.value as String - if emailAddressString == email { - return contact + func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { + for contact in contacts where !contact.emailAddresses.isEmpty { + for emailAddress in contact.emailAddresses { + let emailAddressString = emailAddress.value as String + if emailAddressString == email { + return contact + } } } + return nil } - return nil } -} -enum ContactsListsViewModelState { - case initial - case loading - case result([CNContact]) - case error(AppError) -} + enum ContactsListsViewModelState { + case initial + case loading + case result([CNContact]) + case error(AppError) + } +#endif diff --git a/Sources/OversizeContactsKit/ContactsPicker/EmailPickerView.swift b/Sources/OversizeContactsKit/ContactsPicker/EmailPickerView.swift index 3301f8b..053b539 100644 --- a/Sources/OversizeContactsKit/ContactsPicker/EmailPickerView.swift +++ b/Sources/OversizeContactsKit/ContactsPicker/EmailPickerView.swift @@ -3,168 +3,182 @@ // EmailPickerView.swift // -import Contacts +#if canImport(Contacts) + import Contacts +#endif import OversizeKit import OversizeLocalizable import OversizeUI import SwiftUI -public struct EmailPickerView: View { - @Environment(\.dismiss) private var dismiss - @StateObject private var viewModel: EmailPickerViewModel +#if !os(tvOS) + public struct EmailPickerView: View { + @Environment(\.dismiss) private var dismiss + @StateObject private var viewModel: EmailPickerViewModel - @Binding private var selection: [String] - @State private var selectedEmails: [String] = .init() + @Binding private var selection: [String] + @State private var selectedEmails: [String] = .init() - @FocusState private var isFocusSearth + @FocusState private var isFocusSearth - public init(selection: Binding<[String]>) { - _viewModel = StateObject(wrappedValue: EmailPickerViewModel()) - _selection = selection - } + public init(selection: Binding<[String]>) { + _viewModel = StateObject(wrappedValue: EmailPickerViewModel()) + _selection = selection + } - public var body: some View { - PageView("Add Invitees") { - Group { - switch viewModel.state { - case .initial, .loading: - placeholder() - case let .result(data): - content(data: data) - case let .error(error): - ErrorView(error) + public var body: some View { + PageView("Add Invitees") { + Group { + switch viewModel.state { + case .initial, .loading: + placeholder() + case let .result(data): + content(data: data) + case let .error(error): + ErrorView(error) + } } } + .leadingBar { + BarButton(.close) + } + .trailingBar { + BarButton(.accent("Done", action: { + onDoneAction() + })) + .disabled(selectedEmails.isEmpty && !viewModel.searchText.isEmail) + } + .topToolbar { + TextField("Email or name", text: $viewModel.searchText) + .textFieldStyle(.default) + .focused($isFocusSearth) + #if os(iOS) + .keyboardType(.emailAddress) + #endif + } + .onAppear { + isFocusSearth = true + } + .task { + await viewModel.fetchData() + } } - .leadingBar { - BarButton(.close) - } - .trailingBar { - BarButton(.accent("Done", action: { - onDoneAction() - })) - .disabled(selectedEmails.isEmpty && !viewModel.searchText.isEmail) - } - .topToolbar { - TextField("Email or name", text: $viewModel.searchText) - .textFieldStyle(DefaultPlaceholderTextFieldStyle()) - .focused($isFocusSearth) - #if os(iOS) - .keyboardType(.emailAddress) - #endif - } - .onAppear { - isFocusSearth = true - } - .task { - await viewModel.fetchData() - } - } - @ViewBuilder - private func content(data: [CNContact]) -> some View { - LazyVStack(spacing: .zero) { - newEmailView() + @ViewBuilder + private func content(data: [CNContact]) -> some View { + LazyVStack(spacing: .zero) { + newEmailView() - newSelectedContactsRows(data: data) + newSelectedContactsRows(data: data) - contactsRows(data: data) + contactsRows(data: data) + } } - } - @ViewBuilder - private func newEmailView() -> some View { - if !viewModel.searchText.isEmpty { - Checkbox( - isOn: .constant(viewModel.searchText.isEmail), - label: { - Row(viewModel.searchText, subtitle: "New member") { - Avatar(firstName: viewModel.searchText) + @ViewBuilder + private func newEmailView() -> some View { + if !viewModel.searchText.isEmpty { + Checkbox( + isOn: .constant(viewModel.searchText.isEmail), + label: { + Row(viewModel.searchText, subtitle: "New member") { + Avatar(firstName: viewModel.searchText) + } } - } - ) - .padding(.bottom, .small) + ) + .padding(.bottom, .small) + } } - } - @ViewBuilder - private func newSelectedContactsRows(data: [CNContact]) -> some View { - if !viewModel.lastSelectedEmails.isEmpty { - HStack(spacing: .zero) { - Text("Latest") - Spacer() + @ViewBuilder + private func newSelectedContactsRows(data: [CNContact]) -> some View { + if !viewModel.lastSelectedEmails.isEmpty { + HStack(spacing: .zero) { + Text("Latest") + Spacer() + } + .title3() + .onSurfaceSecondaryForeground() + .padding(.vertical, .xxSmall) + .paddingContent(.horizontal) + + ForEach(viewModel.lastSelectedEmails, id: \.self) { email in + if let contact = viewModel.getContactFromEmail(email: email, contacts: data) { + let emails = contact.emailAddresses + if !emails.isEmpty { + ForEach(emails, id: \.identifier) { email in + emailRow(email: email, contact: contact) + } + } + } else { + let isSelected = selectedEmails.contains(email) + Checkbox( + isOn: Binding( + get: { isSelected }, + set: { _ in onContactClick(email: email) } + ), + label: { + Row(email) { + Avatar(firstName: email) + } + } + ) + } + } } - .title3() - .onSurfaceMediumEmphasisForegroundColor() - .padding(.vertical, .xxSmall) - .paddingContent(.horizontal) + } + + @ViewBuilder + private func contactsRows(data: [CNContact]) -> some View { + if !data.isEmpty { + HStack(spacing: .zero) { + Text("Contacts") + Spacer() + } + .title3() + .onSurfaceSecondaryForeground() + .padding(.vertical, .xxSmall) + .paddingContent(.horizontal) + .padding(.top, viewModel.lastSelectedEmails.isEmpty ? .zero : .small) - ForEach(viewModel.lastSelectedEmails, id: \.self) { email in - if let contact = viewModel.getContactFromEmail(email: email, contacts: data) { + ForEach(data, id: \.identifier) { contact in let emails = contact.emailAddresses if !emails.isEmpty { ForEach(emails, id: \.identifier) { email in emailRow(email: email, contact: contact) } } - } else { - let isSelected = selectedEmails.contains(email) - Checkbox( - isOn: Binding( - get: { isSelected }, - set: { _ in onContactClick(email: email) } - ), - label: { - Row(email) { - Avatar(firstName: email) - } - } - ) } } } - } - @ViewBuilder - private func contactsRows(data: [CNContact]) -> some View { - if !data.isEmpty { - HStack(spacing: .zero) { - Text("Contacts") - Spacer() - } - .title3() - .onSurfaceMediumEmphasisForegroundColor() - .padding(.vertical, .xxSmall) - .paddingContent(.horizontal) - .padding(.top, viewModel.lastSelectedEmails.isEmpty ? .zero : .small) - - ForEach(data, id: \.identifier) { contact in - let emails = contact.emailAddresses - if !emails.isEmpty { - ForEach(emails, id: \.identifier) { email in - emailRow(email: email, contact: contact) - } - } - } - } - } + @ViewBuilder + private func emailRow(email: CNLabeledValue, contact: CNContact) -> some View { + let email = email.value as String + let isSelected = selectedEmails.contains(email) + #if os(iOS) + if let avatarThumbnailData = contact.thumbnailImageData, let avatarThumbnail = UIImage(data: avatarThumbnailData) { + Checkbox(isOn: Binding( + get: { isSelected }, + set: { _ in onContactClick(email: email) } + ), label: { + Row(contact.givenName + " " + contact.familyName, subtitle: email) { + Avatar(firstName: contact.givenName, lastName: contact.familyName, avatar: Image(uiImage: avatarThumbnail)) + } - @ViewBuilder - private func emailRow(email: CNLabeledValue, contact: CNContact) -> some View { - let email = email.value as String - let isSelected = selectedEmails.contains(email) - #if os(iOS) - if let avatarThumbnailData = contact.thumbnailImageData, let avatarThumbnail = UIImage(data: avatarThumbnailData) { - Checkbox(isOn: Binding( - get: { isSelected }, - set: { _ in onContactClick(email: email) } - ), label: { - Row(contact.givenName + " " + contact.familyName, subtitle: email) { - Avatar(firstName: contact.givenName, lastName: contact.familyName, avatar: Image(uiImage: avatarThumbnail)) - } + }) + } else { + Checkbox(isOn: Binding( + get: { isSelected }, + set: { _ in onContactClick(email: email) } + ), label: { + Row(contact.givenName + " " + contact.familyName, subtitle: email) { + Avatar(firstName: contact.givenName, lastName: contact.familyName) + } - }) - } else { + }) + } + #else Checkbox(isOn: Binding( get: { isSelected }, set: { _ in onContactClick(email: email) } @@ -174,59 +188,52 @@ public struct EmailPickerView: View { } }) - } - #else - Checkbox(isOn: Binding( - get: { isSelected }, - set: { _ in onContactClick(email: email) } - ), label: { - Row(contact.givenName + " " + contact.familyName, subtitle: email) { - Avatar(firstName: contact.givenName, lastName: contact.familyName) - } + #endif + } - }) - #endif - } + private func onDoneAction() { + if viewModel.searchText.isEmail { + if !selection.contains(viewModel.searchText) { + selection.append(viewModel.searchText) + } - private func onDoneAction() { - if viewModel.searchText.isEmail { - if !selection.contains(viewModel.searchText) { - selection.append(viewModel.searchText) + if !viewModel.lastSelectedEmails.contains(viewModel.searchText) { + viewModel.lastSelectedEmails.append(viewModel.searchText) + } } - if !viewModel.lastSelectedEmails.contains(viewModel.searchText) { - viewModel.lastSelectedEmails.append(viewModel.searchText) - } - } + if !selectedEmails.isEmpty { + for email in selectedEmails where !selection.contains(email) { + selection.append(email) + } - if !selectedEmails.isEmpty { - for email in selectedEmails where !selection.contains(email) { - selection.append(email) + for email in selectedEmails where !viewModel.lastSelectedEmails.contains(email) { + viewModel.lastSelectedEmails.append(email) + } } - for email in selectedEmails where !viewModel.lastSelectedEmails.contains(email) { - viewModel.lastSelectedEmails.append(email) - } + dismiss() } - dismiss() - } - - private func onContactClick(email: String) { - let isSelected = selectedEmails.contains(email) - if isSelected { - selectedEmails.remove(email) - } else { - selectedEmails.append(email) + private func onContactClick(email: String) { + let isSelected = selectedEmails.contains(email) + if isSelected { + selectedEmails.remove(email) + } else { + selectedEmails.append(email) + } } - } - @ViewBuilder - private func placeholder() -> some View { - LoaderOverlayView() + @ViewBuilder + private func placeholder() -> some View { + #if os(watchOS) + ProgressView() + #else + LoaderOverlayView() + #endif + } } -} - +#endif // struct ContactsPickerView_Previews: PreviewProvider { // static var previews: some View { // EmailPickerView() diff --git a/Sources/OversizeContactsKit/ContactsPicker/EmailPickerViewModel.swift b/Sources/OversizeContactsKit/ContactsPicker/EmailPickerViewModel.swift index 8bbe16b..243dca3 100644 --- a/Sources/OversizeContactsKit/ContactsPicker/EmailPickerViewModel.swift +++ b/Sources/OversizeContactsKit/ContactsPicker/EmailPickerViewModel.swift @@ -3,29 +3,32 @@ // EmailPickerViewModel.swift // -import Contacts +#if canImport(Contacts) + @preconcurrency import Contacts +#endif import Factory import OversizeContactsService import OversizeCore import OversizeModels import SwiftUI -@MainActor -class EmailPickerViewModel: ObservableObject { - @Injected(\.contactsService) private var contactsService: ContactsService - @Published var state = ContactsPickerViewModelState.initial - @Published var searchText: String = .init() +#if !os(tvOS) + @MainActor + class EmailPickerViewModel: ObservableObject { + @Injected(\.contactsService) private var contactsService: ContactsService + @Published var state = ContactsPickerViewModelState.initial + @Published var searchText: String = .init() - @AppStorage("AppState.LastSelectedEmails") var lastSelectedEmails: [String] = .init() + @AppStorage("AppState.LastSelectedEmails") var lastSelectedEmails: [String] = .init() + + func fetchData() async { + state = .loading + let status = await contactsService.requestAccess() + switch status { + case .success: - func fetchData() async { - state = .loading - let status = await contactsService.requestAccess() - switch status { - case .success: - do { let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactThumbnailImageDataKey] - let result = try await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) + let result = await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) switch result { case let .success(data): log("✅ CNContact fetched") @@ -34,30 +37,29 @@ class EmailPickerViewModel: ObservableObject { log("❌ CNContact not fetched (\(error.title))") state = .error(error) } - } catch { - state = .error(.contacts(type: .unknown)) + + case let .failure(error): + state = .error(error) } - case let .failure(error): - state = .error(error) } - } - func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { - for contact in contacts where !contact.emailAddresses.isEmpty { - for emailAddress in contact.emailAddresses { - let emailAddressString = emailAddress.value as String - if emailAddressString == email { - return contact + func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { + for contact in contacts where !contact.emailAddresses.isEmpty { + for emailAddress in contact.emailAddresses { + let emailAddressString = emailAddress.value as String + if emailAddressString == email { + return contact + } } } + return nil } - return nil } -} - -enum ContactsPickerViewModelState { - case initial - case loading - case result([CNContact]) - case error(AppError) -} + + enum ContactsPickerViewModelState { + case initial + case loading + case result([CNContact]) + case error(AppError) + } +#endif diff --git a/Sources/OversizeKit/AdsKit/AdView.swift b/Sources/OversizeKit/AdsKit/AdView.swift index aeb1c44..b2e4836 100644 --- a/Sources/OversizeKit/AdsKit/AdView.swift +++ b/Sources/OversizeKit/AdsKit/AdView.swift @@ -30,6 +30,7 @@ public struct AdView: View { await viewModel.fetchAd() } } + case let .result(appAd): #if os(iOS) Surface { @@ -79,7 +80,7 @@ public struct AdView: View { HStack { Text(appAd.title) .subheadline(.bold) - .onSurfaceHighEmphasisForegroundColor() + .onSurfacePrimaryForeground() Bage(color: .warning) { Text("Our app") @@ -89,7 +90,7 @@ public struct AdView: View { Text(appAd.description) .subheadline() - .onSurfaceMediumEmphasisForegroundColor() + .onSurfaceSecondaryForeground() } .padding(.leading, .xSmall) @@ -100,9 +101,11 @@ public struct AdView: View { } .buttonStyle(.tertiary) .controlBorderShape(.capsule) - .controlSize(.small) .padding(.trailing, .xxxSmall) .loading(isShowProduct) + #if !os(tvOS) + .controlSize(.small) + #endif } } } diff --git a/Sources/OversizeKit/LauncherKit/Launcher.swift b/Sources/OversizeKit/LauncherKit/Launcher.swift index 3b87866..51a704f 100644 --- a/Sources/OversizeKit/LauncherKit/Launcher.swift +++ b/Sources/OversizeKit/LauncherKit/Launcher.swift @@ -14,10 +14,8 @@ public struct Launcher: View { private var onboarding: Onboarding? private let content: Content - private var transaction = Transaction() @StateObject private var viewModel = LauncherViewModel() - @State private var blurRadius: CGFloat = 0 public init(@ViewBuilder content: () -> Content) { self.content = content() @@ -39,40 +37,43 @@ public struct Launcher: View { .appLaunchCover(item: $viewModel.activeFullScreenSheet) { fullScreenCover(sheet: $0) .systemServices() + #if os(macOS) + .frame(width: 840, height: 672) + // .interactiveDismissDisabled(!viewModel.appStateService.isCompletedOnbarding) + #endif } - .onChange(of: viewModel.appStateService.isCompletedOnbarding) { isCompletedOnbarding in + .onChange(of: viewModel.appStateService.isCompletedOnbarding) { _, isCompletedOnbarding in if isCompletedOnbarding, !viewModel.isPremium { viewModel.setPayWall() } else { viewModel.activeFullScreenSheet = nil } } - .onChange(of: scenePhase, perform: { value in + .onChange(of: scenePhase) { _, value in switch value { - case .active, .inactive: - break case .background: viewModel.authState = .locked viewModel.pinCodeField = "" - @unknown default: - log("unknown") + default: + break } - }) + } } + @ViewBuilder var contentView: some View { - Group { - if viewModel.isShowSplashScreen { - SplashScreen() - } else if viewModel.isShowLockscreen { - lockscreenView - } else { - content - .onAppear { - viewModel.reviewService.launchEvent() - viewModel.launcherSheetsChek() + if viewModel.isShowSplashScreen { + SplashScreen() + } else if viewModel.isShowLockscreen { + lockscreenView + } else { + content + .onAppear { + Task { @MainActor in + await viewModel.reviewService.launchEvent() } - } + viewModel.launcherSheetsChek() + } } } @@ -87,14 +88,15 @@ public struct Launcher: View { } private var lockscreenView: some View { - LockscreenView(pinCode: $viewModel.pinCodeField, - state: $viewModel.authState, - title: L10n.Security.enterPINCode, - errorText: L10n.Security.invalidPIN, - pinCodeEnabled: viewModel.settingsService.pinCodeEnabend, - biometricEnabled: viewModel.settingsService.biometricEnabled, - biometricType: viewModel.biometricService.biometricType) - { + LockscreenView( + pinCode: $viewModel.pinCodeField, + state: $viewModel.authState, + title: L10n.Security.enterPINCode, + errorText: L10n.Security.invalidPIN, + pinCodeEnabled: viewModel.settingsService.pinCodeEnabend, + biometricEnabled: viewModel.settingsService.biometricEnabled, + biometricType: viewModel.biometricService.biometricType + ) { viewModel.checkPassword() } biometricAction: { viewModel.appLockValidation() @@ -125,7 +127,6 @@ public extension View { Launcher { self } - .systemServices() } func appLaunch(@ViewBuilder onboarding: @escaping () -> some View) -> some View { @@ -133,7 +134,6 @@ public extension View { self } .onboarding(onboarding: onboarding) - .systemServices() } } @@ -142,8 +142,7 @@ private extension View { item: Binding, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Item) -> some View ) -> some View where Item: Identifiable { #if os(macOS) - interactiveDismissDisabled() - .sheet(item: item, onDismiss: onDismiss, content: content) + sheet(item: item, onDismiss: onDismiss, content: content) #else fullScreenCover(item: item, onDismiss: onDismiss, content: content) #endif diff --git a/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift b/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift index a8e4e0c..8899600 100644 --- a/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift +++ b/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift @@ -49,7 +49,7 @@ public final class LauncherViewModel: ObservableObject { } extension LauncherViewModel { - enum FullScreenSheet: Identifiable, Equatable { + enum FullScreenSheet: Identifiable, Equatable, Sendable { case onboarding case payWall case rate @@ -124,7 +124,9 @@ public extension LauncherViewModel { func setPayWall() { activeFullScreenSheet = nil delay(time: 0.2) { - self.activeFullScreenSheet = .payWall + Task { @MainActor in + self.activeFullScreenSheet = .payWall + } } } diff --git a/Sources/OversizeKit/LauncherKit/RateAppScreen.swift b/Sources/OversizeKit/LauncherKit/RateAppScreen.swift index a762121..b859b0d 100644 --- a/Sources/OversizeKit/LauncherKit/RateAppScreen.swift +++ b/Sources/OversizeKit/LauncherKit/RateAppScreen.swift @@ -17,7 +17,7 @@ struct RateAppScreen: View { VStack { Text("If you love, evaluate)") .largeTitle(.bold) - .onSurfaceHighEmphasisForegroundColor() + .onSurfacePrimaryForeground() Spacer() @@ -29,14 +29,14 @@ struct RateAppScreen: View { Text((Info.app.name ?? "App") + " is developed only one person, and your assessment would very much drop in") .title3() - .onSurfaceHighEmphasisForegroundColor() + .onSurfacePrimaryForeground() Spacer() if let reviewUrl = Info.url.appStoreReview { HStack(spacing: .large) { Link(destination: reviewUrl) { - IconDeprecated(.thumbsUp, color: .onPrimaryHighEmphasis) + IconDeprecated(.thumbsUp, color: .onPrimary) } .buttonStyle(.primary(infinityWidth: false)) .accent() @@ -49,13 +49,15 @@ struct RateAppScreen: View { reviewService.estimate(goodRating: false) dismiss() } label: { - IconDeprecated(.thumbsDown, color: .onSurfaceHighEmphasis) + IconDeprecated(.thumbsDown, color: .onSurfacePrimary) } .buttonStyle(.secondary(infinityWidth: false)) } .controlBorderShape(.capsule) .elevation(.z3) - .controlSize(.large) + #if !os(tvOS) + .controlSize(.large) + #endif } } .multilineTextAlignment(.center) @@ -65,12 +67,14 @@ struct RateAppScreen: View { reviewService.rewiewBunnerClosed() dismiss() } label: { - IconDeprecated(.xMini, color: .onSurfaceHighEmphasis) + IconDeprecated(.xMini, color: .onSurfacePrimary) } .buttonStyle(.tertiary(infinityWidth: false)) - .controlSize(.mini) .controlBorderShape(.capsule) .padding(.medium) + #if !os(tvOS) + .controlSize(.mini) + #endif } // reviewService.rewiewBunnerClosed() } diff --git a/Sources/OversizeKit/LockscreenKit/LockscreenView.swift b/Sources/OversizeKit/LockscreenKit/LockscreenView.swift index 0833b73..cfb01ef 100644 --- a/Sources/OversizeKit/LockscreenKit/LockscreenView.swift +++ b/Sources/OversizeKit/LockscreenKit/LockscreenView.swift @@ -87,7 +87,7 @@ public struct LockscreenView: View { public var body: some View { content() .background(Color.surfacePrimary.ignoresSafeArea(.all)) - .onChange(of: scenePhase) { phase in + .onChange(of: scenePhase) { _, phase in switch phase { case .active: if state == .locked, biometricEnabled { @@ -125,14 +125,14 @@ public struct LockscreenView: View { #else Text(biometricType.rawValue) .title2(.bold) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) #endif } else { Text(biometricType.rawValue) .title2(.semibold) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) } Spacer() @@ -165,7 +165,7 @@ public struct LockscreenView: View { Text(title ?? "") .title2(.bold) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) .opacity(title != nil ? 1 : 0) Spacer() @@ -180,7 +180,7 @@ public struct LockscreenView: View { Text(errorText ?? "") .subheadline() - .errorForegroundColor() + .errorForeground() .opacity(state == .error ? 1 : 0) if isShowTitle { @@ -247,18 +247,18 @@ public struct LockscreenView: View { EmptyView() case .touchID: Image(systemName: "touchid") - .foregroundColor(Color.onBackgroundHighEmphasis) + .foregroundColor(Color.onBackgroundPrimary) .font(.system(size: 26)) .frame(width: 24, height: 24, alignment: .center) case .faceID: Image(systemName: "faceid") .font(.system(size: 26)) - .foregroundColor(Color.onBackgroundHighEmphasis) + .foregroundColor(Color.onBackgroundPrimary) .frame(width: 24, height: 24, alignment: .center) case .opticID: Image(systemName: "opticid") .font(.system(size: 26)) - .foregroundColor(Color.onBackgroundHighEmphasis) + .foregroundColor(Color.onBackgroundPrimary) .frame(width: 24, height: 24, alignment: .center) } } @@ -284,7 +284,6 @@ public struct LockscreenView: View { .frame(width: 12, height: 12) .offset(x: leftOffset) // .animation(Animation.easeInOut(duration: 1).delay(0.2 * Double(number))) - .scaleEffect(shouldAnimate ? 0.5 : 1) .animation(Animation.easeInOut(duration: 0.5) .repeatForever() @@ -332,7 +331,7 @@ public struct NumpadButtonStyle: ButtonStyle { public func makeBody(configuration: Self.Configuration) -> some View { configuration.label .title2() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) .frame(width: 72, height: 72, alignment: .center) .background( Circle() diff --git a/Sources/OversizeKit/SettingsKit/SettingsRouter/ResolveRouter.swift b/Sources/OversizeKit/SettingsKit/SettingsRouter/ResolveRouter.swift new file mode 100644 index 0000000..0da5cbe --- /dev/null +++ b/Sources/OversizeKit/SettingsKit/SettingsRouter/ResolveRouter.swift @@ -0,0 +1,65 @@ +// +// Copyright © 2024 Alexander Romanov +// ResolveRouter.swift, created on 16.05.2024 +// + +import Foundation +import OversizeComponents +import OversizeLocalizable +import OversizeNetwork +import OversizeRouter +import SwiftUI + +extension SettingsScreen: @preconcurrency RoutableView { + @MainActor @ViewBuilder + public func view() -> some View { + switch self { + case .premium: + StoreView() + case .soundAndVibration: + SoundsAndVibrationsSettingsView() + case .appearance: + AppearanceSettingView() + case .sync: + iCloudSettingsView() + case let .premiumFeature(feature: feature): + StoreFeatureDetailView(selection: feature) + case .about: + AboutView() + case .feedback: + FeedbackView() + case .ourResorses: + OurResorsesView() + case .support: + SupportView() + case .border: + BorderSettingView() + case .font: + FontSettingView() + case .radius: + RadiusSettingView() + case .notifications: + NotificationsSettingsView() + case .setPINCode: + SetPINCodeView(action: .set) + case .updatePINCode: + SetPINCodeView(action: .update) + case .security: + SecuritySettingsView() + case let .offer(event: event): + StoreSpecialOfferView(event: event) + case let .webView(url: url): + WebView(url: url) + case let .sendMail(to: to, subject: subject, content: content): + #if os(iOS) + MailView( + to: to, + subject: subject, + content: content + ) + #else + EmptyView() + #endif + } + } +} diff --git a/Sources/OversizeKit/SettingsKit/SettingsRouter/Screens.swift b/Sources/OversizeKit/SettingsKit/SettingsRouter/Screens.swift new file mode 100644 index 0000000..d7d20db --- /dev/null +++ b/Sources/OversizeKit/SettingsKit/SettingsRouter/Screens.swift @@ -0,0 +1,115 @@ +// +// Copyright © 2024 Alexander Romanov +// SettingsScreen.swift, created on 15.04.2024 +// + +import OversizeComponents +import OversizeModels +import OversizeNetwork +import OversizeRouter +import SwiftUI + +public enum SettingsScreen: Routable { + case premium + case premiumFeature(feature: PlistConfiguration.Store.StoreFeature) + case soundAndVibration + case appearance + case sync + case about + case feedback + case ourResorses + case support + case border + case font + case radius + case notifications + case setPINCode + case updatePINCode + case security + case offer(event: Components.Schemas.SpecialOffer) + case webView(url: URL) + case sendMail(to: String, subject: String, content: String) +} + +public extension SettingsScreen { + var id: String { + switch self { + case .premium: + "premium" + case .premiumFeature: + "premiumFeature" + case .soundAndVibration: + "soundAndVibration" + case .appearance: + "appearance" + case .sync: + "sync" + case .about: + "about" + case .feedback: + "feedback" + case .webView: + "webView" + case .ourResorses: + "ourResorses" + case .support: + "support" + case .border: + "border" + case .font: + "font" + case .radius: + "radius" + case .setPINCode: + "setPINCode" + case .updatePINCode: + "updatePINCode" + case .security: + "security" + case .offer: + "offer" + case .sendMail: + "sendMail" + case .notifications: + "notifications" + } + } +} + +// public struct SettingsNavigateAction { +// public typealias Action = (SettingsNavigationType) -> Void +// public let action: Action +// public func callAsFunction(_ navigationType: SettingsNavigationType) { +// action(navigationType) +// } +// } + +// public enum SettingsNavigationType { +// case move(SettingsScreen) +// case backToRoot +// case back(Int = 1) +// case present(_ sheet: SettingsScreen, detents: Set = [.large], indicator: Visibility = .hidden, dismissDisabled: Bool = false) +// case dismiss +// case dismissSheet +// case dismissFullScreenCover +// case dismissDisabled(_ isDismissDisabled: Bool = true) +// case presentHUD(_ text: String, type: HUDMessageType) +// } + +// +// public struct SettingsNavigateEnvironmentKey: EnvironmentKey { +// public static var defaultValue: SettingsNavigateAction = .init(action: { _ in }) +// } +// +// public extension EnvironmentValues { +// var settingsNavigate: SettingsNavigateAction { +// get { self[SettingsNavigateEnvironmentKey.self] } +// set { self[SettingsNavigateEnvironmentKey.self] = newValue } +// } +// } +// +// public extension View { +// func onSettingsNavigate(_ action: @escaping SettingsNavigateAction.Action) -> some View { +// environment(\.settingsNavigate, SettingsNavigateAction(action: action)) +// } +// } diff --git a/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsRouting.swift b/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsRouting.swift new file mode 100644 index 0000000..11aca22 --- /dev/null +++ b/Sources/OversizeKit/SettingsKit/SettingsRouter/SettingsRouting.swift @@ -0,0 +1,20 @@ +// +// Copyright © 2024 Alexander Romanov +// SettingsRouting.swift, created on 10.05.2024 +// + +import OversizeRouter +import SwiftUI + +public struct SettingsRoutingView: View { + public init() {} + + public var body: some View { + RoutingView { + SettingsView { + EmptyView() + } + } + .systemServices() + } +} diff --git a/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift b/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift index eaf732b..d852126 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift @@ -8,223 +8,213 @@ import OversizeComponents import OversizeCore import OversizeLocalizable import OversizeResources +import OversizeRouter import OversizeServices import OversizeUI import SwiftUI // swiftlint:disable all -#if os(iOS) +#if canImport(MessageUI) import MessageUI +#endif - public struct AboutView: View { - @Environment(\.verticalSizeClass) private var verticalSizeClass - @Environment(\.isPortrait) var isPortrait - @Environment(\.presentationMode) var presentationMode - @Environment(\.screenSize) var screenSize - @Environment(\.iconStyle) var iconStyle: IconStyle +public struct AboutView: View { + @Environment(Router.self) var router + @Environment(\.screenSize) var screenSize + @Environment(\.iconStyle) var iconStyle: IconStyle - @StateObject var viewModel: AboutViewModel + @StateObject var viewModel: AboutViewModel - public init() { - _viewModel = StateObject(wrappedValue: AboutViewModel()) - } + public init() { + _viewModel = StateObject(wrappedValue: AboutViewModel()) + } - @State var offset: CGFloat = 0 + @State var offset: CGFloat = 0 - @State var isSharePresented = false - @State private var isShowMail = false + @State var isSharePresented = false + @State private var isShowMail = false - @State var isShowPrivacy = false - @State var isShowTerms = false + @State private var isPresentStoreProduct: Bool = false - @State private var isPresentStoreProduct: Bool = false + var isLargeScreen: Bool { + screenSize.width < 500 ? false : true + } - var isLargeScreen: Bool { - screenSize.width < 500 ? false : true + var oppacity: CGFloat { + if offset < 0 { + return 1 + } else if offset > 500 { + return 0 + } else { + return Double(1 / (offset * 0.01)) } + } - var oppacity: CGFloat { - if offset < 0 { - return 1 - } else if offset > 500 { - return 0 - } else { - return Double(1 / (offset * 0.01)) - } + var blur: CGFloat { + if offset < 0 { + return 0 + } else { + return Double(offset * 0.05) } + } - var blur: CGFloat { - if offset < 0 { - return 0 - } else { - return Double(offset * 0.05) - } - } + #if os(iOS) + let scale = UIScreen.main.scale + #else + let scale: CGFloat = 2 + #endif + public var body: some View { #if os(iOS) - let scale = UIScreen.main.scale + Page(L10n.Settings.about) { + list + .surfaceContentRowMargins() + .task { + await viewModel.fetchApps() + } + } + .backgroundSecondary() + #else - let scale: CGFloat = 2 + list + .navigationTitle(L10n.Settings.about) #endif + } - public var body: some View { - #if os(iOS) - PageView(L10n.Settings.about, onOffsetChanged: { offset = $0 }) { - list - .surfaceContentRowMargins() - .task { - await viewModel.fetchApps() - } - } - .leadingBar { - /* - if !isPortrait, verticalSizeClass == .regular { - EmptyView() - } else { - BarButton(.back) - } - */ - BarButton(.back) - } - .backgroundSecondary() - - #else - list - .navigationTitle(L10n.Settings.about) - #endif - } + private func appLinks() -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: Space.small) { + switch viewModel.state { + case .initial, .loading: + ForEach(0 ... 6, id: \.self) { _ in + RoundedRectangle(cornerRadius: .large, style: .continuous) + .fillSurfaceSecondary() + .frame(width: 74, height: 74) + } + case let .result(apps, _): + ForEach(apps, id: \.appStoreId) { app in + Button { + isPresentStoreProduct = true + } label: { + VStack(spacing: .xSmall) { + CachedAsyncImage(url: URL(string: "https://cdn.oversize.design/assets/apps/" + app.address + "/icon.png"), urlCache: .imageCache, content: { + $0 + .resizable() + .frame(width: 74, height: 74) + .mask(RoundedRectangle(cornerRadius: .large, + style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 16, + style: .continuous) + .stroke(lineWidth: 1) + .opacity(0.15) + ) + + }, placeholder: { + RoundedRectangle(cornerRadius: .large, style: .continuous) + .fillSurfaceSecondary() + .frame(width: 74, height: 74) + }) - private func appLinks() -> some View { - ScrollView(.horizontal, showsIndicators: false) { - HStack(alignment: .top, spacing: Space.small) { - switch viewModel.state { - case .initial, .loading: - ForEach(0 ... 6, id: \.self) { _ in - RoundedRectangle(cornerRadius: .large, style: .continuous) - .fillSurfaceSecondary() - .frame(width: 74, height: 74) - } - case let .result(apps, _): - ForEach(apps, id: \.appStoreId) { app in - Button { - isPresentStoreProduct = true - } label: { - VStack(spacing: .xSmall) { - CachedAsyncImage(url: URL(string: "https://cdn.oversize.design/assets/apps/" + app.address + "/icon.png"), urlCache: .imageCache, content: { - $0 - .resizable() - .frame(width: 74, height: 74) - .mask(RoundedRectangle(cornerRadius: .large, - style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: 16, - style: .continuous) - .stroke(lineWidth: 1) - .opacity(0.15) - ) - - }, placeholder: { - RoundedRectangle(cornerRadius: .large, style: .continuous) - .fillSurfaceSecondary() - .frame(width: 74, height: 74) - }) - - Text(app.name) - .caption(.medium) - .multilineTextAlignment(.center) - .foregroundColor(.onSurfaceMediumEmphasis) - .frame(width: 74) - } + Text(app.name) + .caption(.medium) + .multilineTextAlignment(.center) + .foregroundColor(.onSurfaceSecondary) + .frame(width: 74) } - .buttonStyle(.scale) - .appStoreOverlay(isPresent: $isPresentStoreProduct, appId: app.appStoreId) } - case .error: - EmptyView() + .buttonStyle(.scale) + #if os(iOS) + .appStoreOverlay(isPresent: $isPresentStoreProduct, appId: app.appStoreId) + #endif } + case .error: + EmptyView() + } - if let authorAllApps = Info.url.developerAllApps { - VStack(spacing: .xSmall) { - Link(destination: authorAllApps) { - ZStack { - RoundedRectangle(cornerRadius: 16, style: .continuous) - .foregroundColor(.surfaceSecondary) - .frame(width: 74, height: 74) + if let authorAllApps = Info.url.developerAllApps { + VStack(spacing: .xSmall) { + Link(destination: authorAllApps) { + ZStack { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .foregroundColor(.surfaceSecondary) + .frame(width: 74, height: 74) - IconDeprecated(.externalLink) - } + IconDeprecated(.externalLink) } - - Text("All apps") - .caption(.medium) - .multilineTextAlignment(.center) - .foregroundColor(.onSurfaceMediumEmphasis) - .frame(width: 74) } + + Text("All apps") + .caption(.medium) + .multilineTextAlignment(.center) + .foregroundColor(.onSurfaceSecondary) + .frame(width: 74) } + } - }.padding(.horizontal, .medium) - } - .padding(.bottom, 16) + }.padding(.horizontal, .medium) } + .padding(.bottom, 16) + } - var list: some View { - VStack(spacing: .zero) { - image - .padding(.top, isLargeScreen ? -70 : 0) - - if isLargeScreen { - HStack(spacing: .xxSmall) { - Image.Brands.Oversize.fill - .resizable() - .renderingMode(.template) - .foregroundColor(Color.onSurfaceHighEmphasis) - .frame(width: 32, height: 32) - - Resource.overszieTextLogo - .renderingMode(.template) - .foregroundColor(Color.onSurfaceHighEmphasis) - } - .padding(.top, -40) - .padding(.bottom, .xSmall) - - } else { - VStack(spacing: .xxSmall) { - Image.Brands.Oversize.fill - .resizable() - .renderingMode(.template) - .foregroundColor(Color.onSurfaceHighEmphasis) - .frame(width: 48, height: 48) + var list: some View { + VStack(spacing: .zero) { + image + .padding(.top, isLargeScreen ? -70 : 0) + + if isLargeScreen { + HStack(spacing: .xxSmall) { + Image.Brands.Oversize.fill + .resizable() + .renderingMode(.template) + .foregroundColor(Color.onSurfacePrimary) + .frame(width: 32, height: 32) + + Resource.overszieTextLogo + .renderingMode(.template) + .foregroundColor(Color.onSurfacePrimary) + } + .padding(.top, -40) + .padding(.bottom, .xSmall) - Resource.overszieTextLogo - .renderingMode(.template) - .foregroundColor(Color.onSurfaceHighEmphasis) - } - .padding(.top, 42) - .padding(.bottom, .medium) + } else { + VStack(spacing: .xxSmall) { + Image.Brands.Oversize.fill + .resizable() + .renderingMode(.template) + .foregroundColor(Color.onSurfacePrimary) + .frame(width: 48, height: 48) + + Resource.overszieTextLogo + .renderingMode(.template) + .foregroundColor(Color.onSurfacePrimary) } + .padding(.top, 42) + .padding(.bottom, .medium) + } - Text("The Oversize project is made with love and attention to the design of the forces of only one person") - .title3(.semibold) - .foregroundColor(.onBackgroundHighEmphasis) - .padding(.horizontal, isLargeScreen ? 72 : 52) - .padding(.bottom, .large) - .multilineTextAlignment(.center) - - soclal - .padding(.bottom, .small) - - SectionView { - VStack(spacing: .zero) { - if let reviewUrl = Info.url.appStoreReview, let id = Info.app.appStoreID, !id.isEmpty, let appName = Info.app.name { - Link(destination: reviewUrl) { - Row("Rate \(appName) on App Store") { - rateSettingsIcon.icon() - } + Text("The Oversize project is made with love and attention to the design of the forces of only one person") + .title3(.semibold) + .foregroundColor(.onBackgroundPrimary) + .padding(.horizontal, isLargeScreen ? 72 : 52) + .padding(.bottom, .large) + .multilineTextAlignment(.center) + + soclal + .padding(.bottom, .small) + + SectionView { + VStack(spacing: .zero) { + if let reviewUrl = Info.url.appStoreReview, let id = Info.app.appStoreID, !id.isEmpty, let appName = Info.app.name { + Link(destination: reviewUrl) { + Row("Rate \(appName) on App Store") { + rateSettingsIcon.icon() } - .buttonStyle(.row) } + .buttonStyle(.row) + } + #if os(iOS) if MFMailComposeViewController.canSendMail(), let mail = Info.links?.company.email, let appVersion = Info.app.verstion, @@ -247,73 +237,67 @@ import SwiftUI MailView(to: mail, subject: subject, content: contentPreText) } } + #endif - #if os(iOS) - if let shareUrl = Info.url.appInstallShare, let id = Info.app.appStoreID, !id.isEmpty { - Row(L10n.Settings.shareApplication) { - isSharePresented.toggle() - } leading: { - shareSettingsIcon.icon() - } - .sheet(isPresented: $isSharePresented) { - ActivityViewController(activityItems: [shareUrl]) - .presentationDetents([.medium, .large]) - } + #if os(iOS) + if let shareUrl = Info.url.appInstallShare, let id = Info.app.appStoreID, !id.isEmpty { + Row(L10n.Settings.shareApplication) { + isSharePresented.toggle() + } leading: { + shareSettingsIcon.icon() + } + .sheet(isPresented: $isSharePresented) { + ActivityViewController(activityItems: [shareUrl]) + .presentationDetents([.medium, .large]) } - #endif - } - } - - SectionView { - VStack(spacing: .zero) { - HStack { - Text(L10n.About.otherApplications.uppercased()) - .caption(true) - .foregroundColor(.onSurfaceMediumEmphasis) - .padding(.top, 12) - .padding(.leading, 26) - .padding(.bottom, 18) - Spacer() } + #endif + } + } - appLinks() + SectionView { + VStack(spacing: .zero) { + HStack { + Text(L10n.About.otherApplications.uppercased()) + .caption(true) + .foregroundColor(.onSurfaceSecondary) + .padding(.top, 12) + .padding(.leading, 26) + .padding(.bottom, 18) + Spacer() } + + appLinks() } + } - SectionView { - VStack(spacing: .zero) { - NavigationLink(destination: OurResorsesView()) { - Row("Our open resources") - .rowArrow() - } - .buttonStyle(.row) + SectionView { + VStack(spacing: .zero) { + Row("Our open resources") { + router.move(.ourResorses) + } + .rowArrow() - if let privacyUrl = Info.url.appPrivacyPolicyUrl { - Row(L10n.Store.privacyPolicy) { - isShowPrivacy.toggle() - } - .sheet(isPresented: $isShowPrivacy) { - WebView(url: privacyUrl) - } + if let privacyUrl = Info.url.appPrivacyPolicyUrl { + Row(L10n.Store.privacyPolicy) { + router.present(.webView(url: privacyUrl)) } + } - if let termsOfUde = Info.url.appTermsOfUseUrl { - Row(L10n.Store.termsOfUse) { - isShowTerms.toggle() - } - .sheet(isPresented: $isShowTerms) { - WebView(url: termsOfUde) - } + if let termsOfUde = Info.url.appTermsOfUseUrl { + Row(L10n.Store.termsOfUse) { + router.present(.webView(url: termsOfUde)) } } } - - footer } + + footer } + } - private var soclal: some View { - HStack(spacing: .small) { + private var soclal: some View { + HStack(spacing: .small) { // switch viewModel.state { // case .initial, .loading: // ForEach(0...6, id: \.self) { _ in @@ -349,203 +333,202 @@ import SwiftUI // EmptyView() // } - if let facebook = Info.url.companyFacebook { - Link(destination: facebook) { - // Surface { - HStack { - Spacer() - Image.Brands.Facebook.Circle.fill - .renderingMode(.template) - .foregroundColor(Color.onSurfaceMediumEmphasis) - Spacer() - } - // } + if let facebook = Info.url.companyFacebook { + Link(destination: facebook) { + // Surface { + HStack { + Spacer() + Image.Brands.Facebook.Circle.fill + .renderingMode(.template) + .foregroundColor(Color.onSurfaceSecondary) + Spacer() } + // } } + } - if let instagram = Info.url.companyInstagram { - Link(destination: instagram) { - // Surface { - HStack { - Spacer() - Image.Brands.Instagram.fill - .renderingMode(.template) - .foregroundColor(Color.onSurfaceMediumEmphasis) - Spacer() - } - // } + if let instagram = Info.url.companyInstagram { + Link(destination: instagram) { + // Surface { + HStack { + Spacer() + Image.Brands.Instagram.fill + .renderingMode(.template) + .foregroundColor(Color.onSurfaceSecondary) + Spacer() } + // } } + } - if let twitter = Info.url.companyTwitter { - Link(destination: twitter) { - // Surface { - HStack { - Spacer() - Image.Brands.xCom - .renderingMode(.template) - .foregroundColor(Color.onSurfaceMediumEmphasis) - Spacer() - } - // } + if let twitter = Info.url.companyTwitter { + Link(destination: twitter) { + // Surface { + HStack { + Spacer() + Image.Brands.xCom + .renderingMode(.template) + .foregroundColor(Color.onSurfaceSecondary) + Spacer() } + // } } + } - if let telegramUrl = Info.url.companyTelegram { - Link(destination: telegramUrl) { - // Surface { - HStack { - Spacer() - Image.Brands.Telegram.fill - .renderingMode(.template) - .foregroundColor(Color.onSurfaceMediumEmphasis) - Spacer() - } - // } + if let telegramUrl = Info.url.companyTelegram { + Link(destination: telegramUrl) { + // Surface { + HStack { + Spacer() + Image.Brands.Telegram.fill + .renderingMode(.template) + .foregroundColor(Color.onSurfaceSecondary) + Spacer() } + // } } + } - if let dribbble = Info.url.companyDribbble { - Link(destination: dribbble) { - // Surface { - HStack { - Spacer() - Image.Brands.Dribbble.fill - .renderingMode(.template) - .foregroundColor(Color.onSurfaceMediumEmphasis) - Spacer() - } - // } + if let dribbble = Info.url.companyDribbble { + Link(destination: dribbble) { + // Surface { + HStack { + Spacer() + Image.Brands.Dribbble.fill + .renderingMode(.template) + .foregroundColor(Color.onSurfaceSecondary) + Spacer() } + // } } } - .buttonStyle(.scale) - .frame(maxWidth: 300) - .controlMargin(.xSmall) - .paddingContent(.horizontal) } + .buttonStyle(.scale) + .frame(maxWidth: 300) + .controlMargin(.xSmall) + .paddingContent(.horizontal) + } - private var image: some View { - HStack { - VStack(alignment: .center) { - ZStack(alignment: .top) { - CachedAsyncImage(url: URL(string: "https://cdn.oversize.design/assets/illustrations/scenes/about-layer3.png"), urlCache: .imageCache, scale: scale) { - $0 - .resizable() - .scaledToFit() - .blur(radius: blur) - } placeholder: { - Rectangle() - .fillSurfaceTertiary() - .rotationEffect(.degrees(45)) - .opacity(0) - .frame(height: screenSize.width / 1.16) - } - .offset(y: -offset * 0.1) - - CachedAsyncImage(url: URL(string: "https://cdn.oversize.design/assets/illustrations/scenes/about-layer2.png"), urlCache: .imageCache, scale: scale) { - $0 - .resizable() - .scaledToFit() - .blur(radius: blur) - } placeholder: { - Rectangle() - .fillSurfaceTertiary() - .rotationEffect(.degrees(45)) - .padding(200) - .frame(height: screenSize.width / 1.16) - .overlay { - ProgressView() - } - } + private var image: some View { + HStack { + VStack(alignment: .center) { + ZStack(alignment: .top) { + CachedAsyncImage(url: URL(string: "https://cdn.oversize.design/assets/illustrations/scenes/about-layer3.png"), urlCache: .imageCache, scale: scale) { + $0 + .resizable() + .scaledToFit() + .blur(radius: blur) + } placeholder: { + Rectangle() + .fillSurfaceTertiary() + .rotationEffect(.degrees(45)) + .opacity(0) + .frame(height: screenSize.width / 1.16) + } + .offset(y: -offset * 0.1) - CachedAsyncImage(url: URL(string: "https://cdn.oversize.design/assets/illustrations/scenes/about-layer1.png"), urlCache: .imageCache, scale: scale) { - $0 - .resizable() - .scaledToFit() - .blur(radius: blur) - } placeholder: { - Rectangle() - .fillSurfaceTertiary() - .rotationEffect(.degrees(45)) - .opacity(0) - .frame(height: screenSize.width / 1.16) - } - .offset(y: -(offset * -0.04)) + CachedAsyncImage(url: URL(string: "https://cdn.oversize.design/assets/illustrations/scenes/about-layer2.png"), urlCache: .imageCache, scale: scale) { + $0 + .resizable() + .scaledToFit() + .blur(radius: blur) + } placeholder: { + Rectangle() + .fillSurfaceTertiary() + .rotationEffect(.degrees(45)) + .padding(200) + .frame(height: screenSize.width / 1.16) + .overlay { + ProgressView() + } } - .scaleEffect(screenSize.width < 500 ? 1.4 : 0.9) - .opacity(screenSize.width < 500 ? oppacity : 1) + + CachedAsyncImage(url: URL(string: "https://cdn.oversize.design/assets/illustrations/scenes/about-layer1.png"), urlCache: .imageCache, scale: scale) { + $0 + .resizable() + .scaledToFit() + .blur(radius: blur) + } placeholder: { + Rectangle() + .fillSurfaceTertiary() + .rotationEffect(.degrees(45)) + .opacity(0) + .frame(height: screenSize.width / 1.16) + } + .offset(y: -(offset * -0.04)) } + .scaleEffect(screenSize.width < 500 ? 1.4 : 0.9) + .opacity(screenSize.width < 500 ? oppacity : 1) } } + } - private var footer: some View { - HStack { - Spacer() - - VStack(alignment: .center) { - if let authorLink = Info.links?.company.url { - Link(destination: authorLink) { - if let developerName = Info.developer.name, - let appVersion = Info.app.verstion, - let appName = Info.app.name, - let appBuild = Info.app.build - { - Text("© 2023 \(developerName). \(appName) \(appVersion) (\(appBuild))") - .footnote() - .foregroundColor(.onBackgroundDisabled) - } else { - Text("Developer") - .footnote() - .foregroundColor(.onBackgroundDisabled) - } + private var footer: some View { + HStack { + Spacer() + + VStack(alignment: .center) { + if let authorLink = Info.links?.company.url { + Link(destination: authorLink) { + if let developerName = Info.developer.name, + let appVersion = Info.app.verstion, + let appName = Info.app.name, + let appBuild = Info.app.build + { + Text("© 2024 \(developerName). \(appName) \(appVersion) (\(appBuild))") + .footnote() + .foregroundColor(.onBackgroundTertiary) + } else { + Text("Developer") + .footnote() + .foregroundColor(.onBackgroundTertiary) } } } - - Spacer() } - .padding(.top, Space.small) - .padding(.bottom, 40) - } - var rateSettingsIcon: Image { - switch iconStyle { - case .line: - return Image.Base.heart - case .fill: - return Image.Base.Heart.fill - case .twoTone: - return Image.Base.Heart.TwoTone.fill - } + Spacer() } + .padding(.top, Space.small) + .padding(.bottom, 40) + } - var ideaSettingsIcon: Image { - switch iconStyle { - case .line: - return Image.Electricity.lamp - case .fill: - return Image.Electricity.Lamp.fill - case .twoTone: - return Image.Electricity.Lamp.TwoTone.fill - } + var rateSettingsIcon: Image { + switch iconStyle { + case .line: + return Image.Base.heart + case .fill: + return Image.Base.Heart.fill + case .twoTone: + return Image.Base.Heart.TwoTone.fill } + } - var shareSettingsIcon: Image { - switch iconStyle { - case .line: - return Image.Base.send - case .fill: - return Image.Base.Send.fill - case .twoTone: - return Image.Base.Send.TwoTone.fill - } + var ideaSettingsIcon: Image { + switch iconStyle { + case .line: + return Image.Electricity.lamp + case .fill: + return Image.Electricity.Lamp.fill + case .twoTone: + return Image.Electricity.Lamp.TwoTone.fill } } - struct AboutView_Previews: PreviewProvider { - static var previews: some View { - AboutView() + var shareSettingsIcon: Image { + switch iconStyle { + case .line: + return Image.Base.send + case .fill: + return Image.Base.Send.fill + case .twoTone: + return Image.Base.Send.TwoTone.fill } } -#endif +} + +struct AboutView_Previews: PreviewProvider { + static var previews: some View { + AboutView() + } +} diff --git a/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift b/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift index 976faa1..bd8491f 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift @@ -9,17 +9,18 @@ import OversizeComponents import OversizeLocalizable import OversizeResources +import OversizeRouter import OversizeServices import OversizeUI import SwiftUI public struct FeedbackView: View { + @Environment(Router.self) var router @Environment(\.iconStyle) var iconStyle: IconStyle - @State private var isShowMail = false public init() {} public var body: some View { - PageView("Feedback") { + Page("Feedback") { VStack(spacing: .large) { help @@ -27,10 +28,16 @@ public struct FeedbackView: View { .padding(.bottom, .medium) } } - .trailingBar { - BarButton(.close) - } .backgroundSecondary() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + router.dismiss() + } label: { + Image.Base.close.icon() + } + } + } } private var hero: some View { @@ -63,15 +70,10 @@ public struct FeedbackView: View { let subject = "Feedback" Row(L10n.Settings.feedbakAuthor) { - isShowMail.toggle() + router.present(.sendMail(to: mail, subject: subject, content: contentPreText)) } leading: { mailIcon.icon() } - - .buttonStyle(.row) - .sheet(isPresented: $isShowMail) { - MailView(to: mail, subject: subject, content: contentPreText) - } } else { // Send author if let sendMailUrl = Info.url.developerSendMail { @@ -96,6 +98,7 @@ public struct FeedbackView: View { } } } + .sectionContentCompactRowMargins() } var heartIcon: Image { diff --git a/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift b/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift index 745cb19..ee5cae7 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift @@ -15,17 +15,14 @@ import SwiftUI public struct OurResorsesView: View { @Environment(\.iconStyle) var iconStyle: IconStyle - @State private var isShowMail = false public init() {} public var body: some View { - PageView("Our open resources") { + Page("Our open resources") { links .surfaceContentRowMargins() } - .leadingBar { - BarButton(.back) - } + .backgroundSecondary() } diff --git a/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift b/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift index cc4c5c4..d0aa8cc 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift @@ -9,17 +9,18 @@ import OversizeComponents import OversizeLocalizable import OversizeResources +import OversizeRouter import OversizeServices import OversizeUI import SwiftUI public struct SupportView: View { + @Environment(Router.self) var router @Environment(\.iconStyle) var iconStyle: IconStyle - @State private var isShowMail = false public init() {} public var body: some View { - PageView(L10n.Settings.supportSection) { + Page(L10n.Settings.supportSection) { VStack(spacing: .large) { help @@ -27,10 +28,16 @@ public struct SupportView: View { .padding(.bottom, .medium) } } - .trailingBar { - BarButton(.close) - } .backgroundSecondary() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + router.dismissSheet() + } label: { + Image.Base.close.icon() + } + } + } } private var hero: some View { @@ -54,15 +61,10 @@ public struct SupportView: View { let subject = "Support" Row("Contact Us") { - isShowMail.toggle() + router.present(.sendMail(to: mail, subject: subject, content: contentPreText)) } leading: { mailIcon.icon() } - - .buttonStyle(.row) - .sheet(isPresented: $isShowMail) { - MailView(to: mail, subject: subject, content: contentPreText) - } } else { // Send author if let sendMailUrl = Info.url.developerSendMail { @@ -87,6 +89,7 @@ public struct SupportView: View { } } } + .sectionContentCompactRowMargins() } var heartIcon: Image { @@ -121,15 +124,4 @@ public struct SupportView: View { return Image.Brands.Telegram.twoTone } } - -// var chatIcon: Image { -// switch iconStyle { -// case .line: -// return Icon.Line.Communication.chatDots -// case .solid: -// return Icon.Solid.Communication.chatDots -// case .duotone: -// return Icon.Duotone.Communication.chatDots -// } -// } } diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift index 2423055..89946c5 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift @@ -5,304 +5,256 @@ import OversizeCore import OversizeLocalizable +import OversizeRouter import OversizeServices import OversizeUI import SwiftUI -#if os(iOS) - public class AppIconSettings: ObservableObject { - public var iconNames: [String?] = [nil] - @Published public var currentIndex = 0 - - public init() { - getAlternateIconNames() +public struct AppearanceSettingView: View { + @Environment(Router.self) var router + @Environment(\.theme) private var theme: ThemeSettings + @Environment(\.iconStyle) var iconStyle: IconStyle + @Environment(\.isPremium) var isPremium: Bool - if let currentIcon = UIApplication.shared.alternateIconName { - currentIndex = iconNames.firstIndex(of: currentIcon) ?? 0 - } - } + #if os(iOS) + @StateObject var iconSettings = AppIconSettings() + #endif - private func getAlternateIconNames() { - if let iconCount = FeatureFlags.app.alternateAppIcons, iconCount != 0 { - for index in 1 ... iconCount { - iconNames.append("AlternateAppIcon\(index)") - } - } - } - } -#endif + private let columns = [ + GridItem(.adaptive(minimum: 78)), + ] -#if os(iOS) - public struct AppearanceSettingView: View { - @Environment(\.verticalSizeClass) private var verticalSizeClass - @Environment(\.presentationMode) var presentationMode - @Environment(\.theme) private var theme: ThemeSettings - @Environment(\.isPortrait) var isPortrait - @Environment(\.iconStyle) var iconStyle: IconStyle - @Environment(\.isPremium) var isPremium: Bool + public init() {} + public var body: some View { #if os(iOS) - @StateObject var iconSettings = AppIconSettings() - #endif - - // swiftlint:disable trailing_comma - - @State var offset = CGPoint(x: 0, y: 0) - @State var pageDestenation: Destenation? - @State var isShowPremium: Bool = .init(false) - private let columns = [ - GridItem(.adaptive(minimum: 78)), - ] - enum Destenation { - case font - case border - case radius - } - - public init() {} - public var body: some View { - #if os(iOS) - PageView(L10n.Settings.apperance) { - iOSSettings - .surfaceContentRowMargins() - } - .leadingBar { - /* - if !isPortrait, verticalSizeClass == .regular { - EmptyView() - } else { - BarButton(.back) - } - */ - BarButton(.back) - } - .backgroundSecondary() + Page(L10n.Settings.apperance) { + iOSSettings + .surfaceContentRowMargins() + } + .backgroundSecondary() - #else - macSettings - #endif - } + #else + macSettings + #endif + } - #if os(iOS) - private var iOSSettings: some View { - VStack(alignment: .center, spacing: 0) { - apperance + #if os(iOS) + private var iOSSettings: some View { + LazyVStack(alignment: .leading, spacing: 0) { + apperance - accentColor + accentColor - advanded + advanded - if iconSettings.iconNames.count > 1 { - appIcon - } + if iconSettings.iconNames.count > 1 { + appIcon } - .preferredColorScheme(theme.appearance.colorScheme) - .accentColor(theme.accentColor) } - #endif - - private var macSettings: some View { - VStack(alignment: .center, spacing: 0) { - advanded - } - .frame(width: 400, height: 300) - // swiftlint:disable multiple_closures_with_trailing_closure superfluous_disable_command - - .navigationTitle("Appearance") .preferredColorScheme(theme.appearance.colorScheme) + .accentColor(theme.accentColor) } + #endif - #if os(iOS) - private var apperance: some View { - SectionView { - HStack { - ForEach(Appearance.allCases, id: \.self) { appearance in + private var macSettings: some View { + VStack(alignment: .center, spacing: 0) { + advanded + } + .frame(width: 400, height: 300) + // swiftlint:disable multiple_closures_with_trailing_closure superfluous_disable_command + .navigationTitle("Appearance") + .preferredColorScheme(theme.appearance.colorScheme) + } - HStack { - Spacer() + private var apperance: some View { + SectionView { + HStack { + ForEach(Appearance.allCases, id: \.self) { appearance in - VStack(spacing: .zero) { - Text(appearance.name) - .foregroundColor(.onSurfaceHighEmphasis) - .font(.subheadline) - .bold() + HStack { + Spacer() - appearance.image - .padding(.vertical, .medium) + VStack(spacing: .zero) { + Text(appearance.name) + .foregroundColor(.onSurfacePrimary) + .font(.subheadline) + .bold() - if appearance == theme.appearance { - IconDeprecated(.checkCircle, color: Color.accent) - } else { - IconDeprecated(.circle, color: .onSurfaceMediumEmphasis) - } - } - Spacer() - } - .onTapGesture { - theme.appearance = appearance + appearance.image + .padding(.vertical, .medium) + + if appearance == theme.appearance { + IconDeprecated(.checkCircle, color: Color.accent) + } else { + IconDeprecated(.circle, color: .onSurfaceSecondary) } } - }.padding(.vertical, .xSmall) + Spacer() + } + .onTapGesture { + theme.appearance = appearance + } } - } - #endif + }.padding(.vertical, .xSmall) + } + } - #if os(iOS) - private var accentColor: some View { - SectionView("Accent color") { - ColorSelector(selection: theme.$accentColor) - } + #if os(iOS) + private var accentColor: some View { + SectionView("Accent color") { + ColorSelector(selection: theme.$accentColor) } + } - #endif - - #if os(iOS) - private var appIcon: some View { - SectionView("App icon") { - LazyVGrid(columns: columns, spacing: 24) { - ForEach(0 ..< iconSettings.iconNames.count, id: \.self) { index in - HStack { - Image(uiImage: UIImage(named: iconSettings.iconNames[index] - ?? "AppIcon") ?? UIImage()) - .renderingMode(.original) - .resizable() - .scaledToFit() - .frame(width: 78, height: 78) - .cornerRadius(18) - .overlay( - RoundedRectangle(cornerRadius: 20) - .stroke(index == iconSettings.currentIndex ? Color.accent : Color.border, - lineWidth: index == iconSettings.currentIndex ? 3 : 1) - ) - .onTapGesture { - if index != 0, isPremium == false { - isShowPremium.toggle() - } else { - let defaultIconIndex = iconSettings.iconNames - .firstIndex(of: UIApplication.shared.alternateIconName) ?? 0 - if defaultIconIndex != index { - // swiftlint:disable line_length - UIApplication.shared.setAlternateIconName(iconSettings.iconNames[index]) { error in - if let error { - log(error.localizedDescription) - } else { - log("Success! You have changed the app icon.") - } + #endif + + #if os(iOS) + private var appIcon: some View { + SectionView("App icon") { + LazyVGrid(columns: columns, spacing: 24) { + ForEach(0 ..< iconSettings.iconNames.count, id: \.self) { index in + HStack { + Image(uiImage: UIImage(named: iconSettings.iconNames[index] + ?? "AppIcon") ?? UIImage()) + .renderingMode(.original) + .resizable() + .scaledToFit() + .frame(width: 78, height: 78) + .cornerRadius(18) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(index == iconSettings.currentIndex ? Color.accent : Color.border, + lineWidth: index == iconSettings.currentIndex ? 3 : 1) + ) + .onTapGesture { + if index != 0, isPremium == false { + router.present(.premium) + } else { + let defaultIconIndex = iconSettings.iconNames + .firstIndex(of: UIApplication.shared.alternateIconName) ?? 0 + if defaultIconIndex != index { + // swiftlint:disable line_length + UIApplication.shared.setAlternateIconName(iconSettings.iconNames[index]) { error in + if let error { + log(error.localizedDescription) + } else { + log("Success! You have changed the app icon.") } } } } - } - .padding(3) + } } + .padding(3) } - .padding() } - .sheet(isPresented: $isShowPremium, content: { - StoreView() - .closable() - }) + .padding() } - #endif - - private var advanded: some View { - SectionView("Advanced settings") { - ZStack { - NavigationLink(destination: FontSettingView(), - tag: .font, - selection: $pageDestenation) { EmptyView() } - - NavigationLink(destination: BorderSettingView(), - tag: .border, - selection: $pageDestenation) { EmptyView() } - - NavigationLink(destination: RadiusSettingView(), - tag: .radius, - selection: $pageDestenation) { EmptyView() } - - VStack(spacing: .zero) { - Row("Fonts") { - pageDestenation = .font - } leading: { - textIcon.icon() - } - .rowArrow() - .premium() - .onPremiumTap() - - Switch(isOn: theme.$borderApp) { - Row("Borders") { - pageDestenation = .border - } leading: { - borderIcon.icon() - } - .premium() - } - .onPremiumTap() -// Row("Borders", leadingType: .image(borderIcon), trallingType: .toggleWithArrowButton(isOn: theme.$borderApp, action: { -// pageDestenation = .border -// })) { -// pageDestenation = .border -// } - - .onChange(of: theme.borderApp) { value in - theme.borderSurface = value - theme.borderButtons = value - theme.borderControls = value - theme.borderTextFields = value - } - - Row("Radius") { - pageDestenation = .radius - } leading: { - radiusIcon.icon() - } - .rowArrow() - .premium() - .onPremiumTap() + } + #endif + + private var advanded: some View { + SectionView("Advanced settings") { + VStack(spacing: .zero) { + Row("Fonts") { + router.move(.font) + } leading: { + textIcon.icon() + } + .rowArrow() + .premium() + .onPremiumTap() + + Switch(isOn: theme.$borderApp) { + Row("Borders") { + router.move(.border) + } leading: { + borderIcon.icon() } + .premium() + } + .onPremiumTap() + .onChange(of: theme.borderApp) { _, value in + theme.borderSurface = value + theme.borderButtons = value + theme.borderControls = value + theme.borderTextFields = value + } + + Row("Radius") { + router.move(.radius) + } leading: { + radiusIcon.icon() } + .rowArrow() + .premium() + .onPremiumTap() } } + } - var textIcon: Image { - switch iconStyle { - case .line: - return Image.Editor.Font.square - case .fill: - return Image.Editor.Font.Square.fill - case .twoTone: - return Image.Editor.Font.Square.TwoTone.fill - } + var textIcon: Image { + switch iconStyle { + case .line: + return Image.Editor.Font.square + case .fill: + return Image.Editor.Font.Square.fill + case .twoTone: + return Image.Editor.Font.Square.TwoTone.fill } + } - var borderIcon: Image { - switch iconStyle { - case .line: - return Image.Design.verticalMirror - case .fill: - return Image.Editor.Font.Square.fill - case .twoTone: - return Image.Editor.Font.Square.TwoTone.fill - } + var borderIcon: Image { + switch iconStyle { + case .line: + return Image.Design.verticalMirror + case .fill: + return Image.Editor.Font.Square.fill + case .twoTone: + return Image.Editor.Font.Square.TwoTone.fill } + } - var radiusIcon: Image { - switch iconStyle { - case .line: - return Image.Design.path - case .fill: - return Image.Design.Path.fill - case .twoTone: - return Image.Design.Path.twoTone - } + var radiusIcon: Image { + switch iconStyle { + case .line: + return Image.Design.path + case .fill: + return Image.Design.Path.fill + case .twoTone: + return Image.Design.Path.twoTone } } +} + +#if os(iOS) + @MainActor + public class AppIconSettings: ObservableObject { + public var iconNames: [String?] = [nil] + @Published public var currentIndex = 0 + + public init() { + getAlternateIconNames() - struct SettingsThemeView_Previews: PreviewProvider { - static var previews: some View { - AppearanceSettingView() - .previewPhones() + if let currentIcon = UIApplication.shared.alternateIconName { + currentIndex = iconNames.firstIndex(of: currentIcon) ?? 0 + } + } + + private func getAlternateIconNames() { + if let iconCount = FeatureFlags.app.alternateAppIcons, iconCount != 0 { + for index in 1 ... iconCount { + iconNames.append("AlternateAppIcon\(index)") + } + } } } #endif + +struct SettingsThemeView_Previews: PreviewProvider { + static var previews: some View { + AppearanceSettingView() + .previewPhones() + } +} diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettongView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettingView.swift similarity index 94% rename from Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettongView.swift rename to Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettingView.swift index 5092940..6a41ab4 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettongView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettingView.swift @@ -1,6 +1,6 @@ // // Copyright © 2022 Alexander Romanov -// BorderSettongView.swift +// BorderSettingView.swift // import OversizeUI @@ -31,14 +31,14 @@ public struct BorderSettingView: View { SectionView { VStack(spacing: .zero) { Toggle("Borders in app", isOn: theme.$borderApp) - .onChange(of: theme.borderApp) { value in + .onChange(of: theme.borderApp) { _, value in theme.borderSurface = value theme.borderButtons = value theme.borderControls = value theme.borderTextFields = value } .headline() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) .padding(.horizontal, .medium) .padding(.vertical, .small) @@ -50,13 +50,13 @@ public struct BorderSettingView: View { HStack { Text("Size") .subheadline() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Spacer() Text(String(format: "%.1f", theme.borderSize) + " px") .subheadline() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) } Slider(value: theme.$borderSize, in: 0.5 ... 2, step: 0.5) diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift index ed4fba7..ad9c8cf 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift @@ -56,11 +56,11 @@ public struct FontSettingView: View { VStack(alignment: .leading, spacing: 8) { Text("Aa") .font(.system(size: 34, weight: .heavy, design: fontStyle.system)) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Text(fontStyle.rawValue.capitalizingFirstLetter()) .font(.system(size: 16, weight: .medium, design: fontStyle.system)) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) } Spacer() }.padding() @@ -74,11 +74,11 @@ public struct FontSettingView: View { VStack(alignment: .leading, spacing: 8) { Text("Aa") .font(.system(size: 34, weight: .heavy, design: fontStyle.system)) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Text(fontStyle.rawValue.capitalizingFirstLetter()) .font(.system(size: 16, weight: .medium, design: fontStyle.system)) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) } Spacer() }.padding() @@ -91,16 +91,16 @@ public struct FontSettingView: View { Text("Button".uppercased()) .bold() .caption() - .onBackgroundMediumEmphasisForegroundColor() + .onBackgroundSecondaryForeground() SegmentedPickerSelector(FontDesignType.allCases, selection: theme.$fontButton) { fontStyle, _ in VStack(alignment: .center, spacing: 8) { Text("Aa") .font(.system(size: 28, weight: .heavy, design: fontStyle.system)) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Text(fontStyle.rawValue.capitalizingFirstLetter()) .font(.system(size: 12, weight: .medium, design: fontStyle.system)) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) } } .segmentedControlStyle(.island(selected: .graySurface)) @@ -110,16 +110,16 @@ public struct FontSettingView: View { Text("Overline & caption".uppercased()) .bold() .caption() - .onBackgroundMediumEmphasisForegroundColor() + .onBackgroundSecondaryForeground() SegmentedPickerSelector(FontDesignType.allCases, selection: theme.$fontOverline) { fontStyle, _ in VStack(alignment: .center, spacing: 8) { Text("Aa") .font(.system(size: 28, weight: .heavy, design: fontStyle.system)) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Text(fontStyle.rawValue.capitalizingFirstLetter()) .font(.system(size: 12, weight: .medium, design: fontStyle.system)) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) } } .segmentedControlStyle(.island(selected: .graySurface)) @@ -138,34 +138,34 @@ extension FontSettingView { Text("Overline".uppercased()) .bold() .caption() - .onBackgroundMediumEmphasisForegroundColor() + .onBackgroundSecondaryForeground() Text("Large title") .largeTitle() - .onBackgroundHighEmphasisForegroundColor() + .onBackgroundPrimaryForeground() Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") .body() - .onBackgroundMediumEmphasisForegroundColor() + .onBackgroundSecondaryForeground() } VStack(alignment: .leading, spacing: Space.xxSmall.rawValue) { Text("Title") .title3() - .onBackgroundHighEmphasisForegroundColor() + .onBackgroundPrimaryForeground() Text("Subtitle") .headline() - .onBackgroundHighEmphasisForegroundColor() + .onBackgroundPrimaryForeground() Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") .bold() .subheadline() - .onBackgroundHighEmphasisForegroundColor() + .onBackgroundPrimaryForeground() Text("Button") .body() - .onBackgroundHighEmphasisForegroundColor() + .onBackgroundPrimaryForeground() .padding(.top, .xxxSmall) } } diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift index c42d83a..1d63b74 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift @@ -6,7 +6,7 @@ import OversizeUI import SwiftUI -struct RadiusSettingView: View { +public struct RadiusSettingView: View { @Environment(\.theme) private var theme: ThemeSettings public init() {} @@ -32,13 +32,13 @@ struct RadiusSettingView: View { HStack { Text("Size") .subheadline() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Spacer() Text(String(format: "%.0f", theme.radius) + " px") .subheadline() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) } Slider(value: theme.$radius, in: 0 ... 12, step: 4) @@ -53,12 +53,6 @@ struct RadiusSettingView: View { Spacer() } -// .navigationBar("Radius", style: .fixed($offset)) { -// BarButton(.back) -// } trailingBar: {} bottomBar: {} -// .background(Color.backgroundSecondary.ignoresSafeArea(.all)) -// .preferredColorScheme(theme.appearance.colorScheme) -// .animation(.easeIn(duration: 0.2)) } } diff --git a/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift index 6129783..66cc4c8 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Notifications/NotificationsSettingsView.swift @@ -9,33 +9,27 @@ import OversizeUI import SwiftUI // swiftlint:disable line_length -#if os(iOS) - public struct NotificationsSettingsView: View { - @Environment(\.presentationMode) var presentationMode - @Environment(\.verticalSizeClass) private var verticalSizeClass - @Environment(\.isPortrait) var isPortrait - @StateObject var settingsService = SettingsService() - @State var offset = CGPoint(x: 0, y: 0) - public var body: some View { - PageView(L10n.Settings.notifications) { - soundsAndVibrations - .surfaceContentRowMargins() - } - .leadingBar { - BarButton(.back) - } - .backgroundSecondary() +public struct NotificationsSettingsView: View { + @StateObject var settingsService = SettingsService() + + public init() {} + + public var body: some View { + Page(L10n.Settings.notifications) { + soundsAndVibrations + .surfaceContentRowMargins() } + .backgroundSecondary() } +} - extension NotificationsSettingsView { - private var soundsAndVibrations: some View { - SectionView { - VStack(spacing: .zero) { - Switch(L10n.Settings.notifications, isOn: $settingsService.notificationEnabled) - } +extension NotificationsSettingsView { + private var soundsAndVibrations: some View { + SectionView { + VStack(spacing: .zero) { + Switch(L10n.Settings.notifications, isOn: $settingsService.notificationEnabled) } } } -#endif +} diff --git a/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift b/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift index 8bfb6a8..c0484ee 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift @@ -4,12 +4,14 @@ // import OversizeLocalizable +import OversizeRouter import OversizeUI import SwiftUI public struct SetPINCodeView: View { + @Environment(Router.self) var router + @Environment(HUDRouter.self) private var hudRouter: HUDRouter @ObservedObject var viewModel: SetPINCodeViewModel - @EnvironmentObject private var hud: HUDDeprecated @Environment(\.dismiss) var dismiss public init(action: PINCodeAction) { @@ -17,20 +19,16 @@ public struct SetPINCodeView: View { } public var body: some View { - ZStack(alignment: .leading) { - stateView(state: viewModel.state) - - VStack(alignment: .leading) { - Button { - dismiss() - } label: { - IconDeprecated(.xMini, color: .onSurfaceHighEmphasis) + stateView(state: viewModel.state) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + router.dismiss() + } label: { + Image.Base.close.icon() + } } - .buttonStyle(.secondary) - - Spacer() - }.padding(20) - } + } } @ViewBuilder @@ -55,6 +53,7 @@ public struct SetPINCodeView: View { { viewModel.checkNewPINCode() } biometricAction: {} + case .confirmNewPINField: LockscreenView(pinCode: $viewModel.confirmNewCodeField, state: $viewModel.authState, @@ -69,9 +68,9 @@ public struct SetPINCodeView: View { dismiss() switch viewModel.action { case .set: - hud.show(title: L10n.Security.createPINCode, icon: .check) + hudRouter.present(L10n.Security.createPINCode) case .update: - hud.show(title: L10n.Security.pinChanged, icon: .check) + hudRouter.present(L10n.Security.pinChanged) } case false: diff --git a/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeViewModel.swift b/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeViewModel.swift index a75ebd3..3ef7f19 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeViewModel.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeViewModel.swift @@ -9,17 +9,18 @@ import OversizeServices import OversizeUI import SwiftUI -public enum PINCodeAction: Identifiable { +public enum PINCodeAction: Identifiable, Sendable { case set, update public var id: Int { hashValue } } -public enum SetPINCodeViewState { +public enum SetPINCodeViewState: Sendable { case oldPINField, newPINField, confirmNewPINField } +@MainActor public final class SetPINCodeViewModel: ObservableObject { @Published public var settingsStore: SettingsService diff --git a/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift index bc89bab..dc09593 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift @@ -5,107 +5,97 @@ import Factory import OversizeLocalizable +import OversizeRouter import OversizeServices import OversizeUI import SwiftUI // swiftlint:disable line_length -#if os(iOS) - public struct SecuritySettingsView: View { - @Injected(\.biometricService) var biometricService - @Environment(\.verticalSizeClass) private var verticalSizeClass - @Environment(\.isPortrait) var isPortrait - @Environment(\.presentationMode) var presentationMode - @StateObject var settingsService = SettingsService() - - @State var offset = CGPoint(x: 0, y: 0) - @State var isSetPINCodeSheet: PINCodeAction? - - public init() {} - - public var body: some View { - PageView(L10n.Security.title) { - iOSSettings - .surfaceContentRowMargins() - } - .leadingBar { - BarButton(.back) - } - .backgroundSecondary() + +public struct SecuritySettingsView: View { + @Injected(\.biometricService) var biometricService + @Environment(Router.self) var router + @StateObject var settingsService = SettingsService() + + public init() {} + + public var body: some View { + Page(L10n.Security.title) { + iOSSettings + .surfaceContentRowMargins() } + .backgroundSecondary() } +} - extension SecuritySettingsView { - private var iOSSettings: some View { - VStack(alignment: .center, spacing: 0) { - faceID +extension SecuritySettingsView { + private var iOSSettings: some View { + VStack(alignment: .center, spacing: 0) { + faceID - additionally - } + // additionally } } - - extension SecuritySettingsView { - private var faceID: some View { - SectionView(L10n.Settings.entrance) { - VStack(spacing: .zero) { - if FeatureFlags.secure.faceID.valueOrFalse, biometricService.checkIfBioMetricAvailable() { - Switch(isOn: - Binding(get: { - settingsService.biometricEnabled - }, set: { - biometricChange(state: $0) - }) - ) { - Row(biometricService.biometricType.rawValue) { - Image(systemName: biometricImageName) - .foregroundColor(Color.onBackgroundHighEmphasis) - .font(.system(size: 20, weight: .semibold)) - .frame(width: 24, height: 24, alignment: .center) - } +} + +extension SecuritySettingsView { + private var faceID: some View { + SectionView(L10n.Settings.entrance) { + VStack(spacing: .zero) { + if FeatureFlags.secure.faceID.valueOrFalse, biometricService.checkIfBioMetricAvailable() { + Switch(isOn: + Binding(get: { + settingsService.biometricEnabled + }, set: { + biometricChange(state: $0) + }) + ) { + Row(biometricService.biometricType.rawValue) { + Image(systemName: biometricImageName) + .foregroundColor(Color.onBackgroundPrimary) + .font(.system(size: 20, weight: .semibold)) + .frame(width: 24, height: 24, alignment: .center) } } + } - if FeatureFlags.secure.lookscreen.valueOrFalse { - Switch(isOn: - Binding(get: { - settingsService.pinCodeEnabend - }, set: { - if settingsService.isSetedPinCode() { - settingsService.pinCodeEnabend = $0 - } else { - isSetPINCodeSheet = .set - } - }) - ) { - Row(L10n.Security.pinCode) { - Image.Security.lock.icon() + if FeatureFlags.secure.lookscreen.valueOrFalse { + Switch(isOn: + Binding(get: { + settingsService.pinCodeEnabend + }, set: { + if settingsService.isSetedPinCode() { + settingsService.pinCodeEnabend = $0 + } else { + router.present(.setPINCode) } - }.sheet(item: $isSetPINCodeSheet) { sheet in - SetPINCodeView(action: sheet) - .systemServices() + }) + ) { + Row(L10n.Security.pinCode) { + Image.Security.lock.icon() } + } - if settingsService.isSetedPinCode() { - Row(L10n.Security.changePINCode) { - isSetPINCodeSheet = .update - } - .rowArrow() + if settingsService.isSetedPinCode() { + Row(L10n.Security.changePINCode) { + router.present(.updatePINCode) } + .rowArrow() } } } } + } - private func biometricChange(state: Bool) { - Task { - await settingsService.biometricChange(state) - } + private func biometricChange(state: Bool) { + Task { + await settingsService.biometricChange(state) } + } - private var additionally: some View { - SectionView(L10n.Settings.additionally) { - VStack(spacing: .zero) { + private var additionally: some View { + SectionView(L10n.Settings.additionally) { + VStack(spacing: .zero) { // if FeatureFlags.secure.lookscreen.valueOrFalse { // Row(L10n.Security.inactiveAskPassword, trallingType: .toggle(isOn: $settingsStore.askPasswordWhenInactiveEnabend)) // } @@ -136,34 +126,33 @@ import SwiftUI // Row(L10n.Security.facedownLock, trallingType: .toggle(isOn: $settingsStore.lookScreenDownEnabend)) // } // - if FeatureFlags.secure.blurMinimize.valueOrFalse { - Switch(isOn: $settingsService.blurMinimizeEnabend) { - Row(L10n.Security.blurMinimize) - .premium() - } - .onPremiumTap() + if FeatureFlags.secure.blurMinimize.valueOrFalse { + Switch(isOn: $settingsService.blurMinimizeEnabend) { + Row(L10n.Security.blurMinimize) + .premium() } + .onPremiumTap() + } // if FeatureFlags.secure.lookscreen.valueOrFalse { // Row(L10n.Security.authHistory, trallingType: .toggle(isOn: $settingsService.authHistoryEnabend)) // .premium() // .onPremiumTap() // } - } } } + } - private var biometricImageName: String { - switch biometricService.biometricType { - case .none: - return "" - case .touchID: - return "touchid" - case .faceID: - return "faceid" - case .opticID: - return "opticid" - } + private var biometricImageName: String { + switch biometricService.biometricType { + case .none: + return "" + case .touchID: + return "touchid" + case .faceID: + return "faceid" + case .opticID: + return "opticid" } } -#endif +} diff --git a/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift index 0a8d144..22fd90d 100644 --- a/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift @@ -5,92 +5,42 @@ import OversizeLocalizable import OversizeResources +import OversizeRouter import OversizeServices import OversizeUI import SwiftUI // swiftlint:disable line_length -#if os(iOS) - public struct SettingsView: View { - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @Environment(\.verticalSizeClass) private var verticalSizeClass - @Environment(\.presentationMode) var presentationMode - @Environment(\.iconStyle) var iconStyle: IconStyle - @Environment(\.theme) var theme: ThemeSettings - @StateObject var settingsService = SettingsService() - @EnvironmentObject var hudState: HUDDeprecated - - let appSection: AppSection - let headSection: HeadSection - - @State private var isPortrait = false - @State private var isShowSupport = false - @State private var isShowFeedback = false - - var isShowArrow: Bool { - #if os(iOS) - if !isPortrait, verticalSizeClass == .regular { - return false - } else { - return true - } - #else - return true +public struct SettingsView: View { + @Environment(Router.self) var router + @Environment(\.iconStyle) var iconStyle: IconStyle + @Environment(\.theme) var theme: ThemeSettings + @StateObject var settingsService = SettingsService() + + let appSection: AppSection + let headSection: HeadSection + + public init( + @ViewBuilder appSection: () -> AppSection, + @ViewBuilder headSection: () -> HeadSection + ) { + self.appSection = appSection() + self.headSection = headSection() + } - #endif - } + public var body: some View { + #if os(iOS) - public init(@ViewBuilder appSection: () -> AppSection, - @ViewBuilder headSection: () -> HeadSection) - { - self.appSection = appSection() - self.headSection = headSection() - } + Page(L10n.Settings.title) { + iOSSettings + }.backgroundSecondary() - public var body: some View { - #if os(iOS) - Group { - if !isPortrait, verticalSizeClass == .regular { - Group { - PageView(L10n.Settings.title) { - iOSSettings - }.backgroundSecondary() + #else + macSettings - AppearanceSettingView() - } - .navigationable() - .navigationViewStyle(DoubleColumnNavigationViewStyle()) - } else { - Group { - PageView(L10n.Settings.title) { - iOSSettings - } - .backgroundSecondary() - } - .navigationable() - .navigationViewStyle(StackNavigationViewStyle()) - } - } - .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in - setOrientation() - } - .onAppear { - setOrientation() - } - .portraitMode(isPortrait) - - #else - macSettings - - #endif - } - - func setOrientation() { - guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } - isPortrait = scene.interfaceOrientation.isPortrait - } + #endif } -#endif +} // iOS Settings #if os(iOS) @@ -116,312 +66,272 @@ import SwiftUI } #endif -#if os(iOS) - // Sections - extension SettingsView { - private var head: some View { - SectionView { - headSection - } +extension SettingsView { + private var head: some View { + SectionView { + headSection } + } - private var app: some View { - SectionView("General") { - VStack(spacing: .zero) { - if FeatureFlags.app.apperance.valueOrFalse { - NavigationLink(destination: AppearanceSettingView() - ) { - Row(L10n.Settings.apperance) { - apperanceSettingsIcon.icon() - } - .rowArrow(isShowArrow) - } - .buttonStyle(.row) + private var app: some View { + SectionView("General") { + VStack(spacing: .zero) { + if FeatureFlags.app.apperance.valueOrFalse { + Row(L10n.Settings.apperance) { + router.move(.appearance) + } leading: { + apperanceSettingsIcon.icon() } + .rowArrow() + } - if FeatureFlags.app.сloudKit.valueOrFalse || FeatureFlags.app.healthKit.valueOrFalse { - NavigationLink(destination: iCloudSettingsView() - ) { - Row(L10n.Title.synchronization) { - cloudKitIcon.icon() - } - .rowArrow(isShowArrow) - } - .buttonStyle(.row) + if FeatureFlags.app.сloudKit.valueOrFalse || FeatureFlags.app.healthKit.valueOrFalse { + Row(L10n.Title.synchronization) { + router.move(.sync) + } leading: { + cloudKitIcon.icon() } + .rowArrow() + } - if FeatureFlags.secure.faceID.valueOrFalse - || FeatureFlags.secure.lookscreen.valueOrFalse - || FeatureFlags.secure.CVVCodes.valueOrFalse - || FeatureFlags.secure.alertSecureCodes.valueOrFalse - || FeatureFlags.secure.blurMinimize.valueOrFalse - || FeatureFlags.secure.bruteForceSecure.valueOrFalse - || FeatureFlags.secure.photoBreaker.valueOrFalse - { - NavigationLink(destination: SecuritySettingsView() - ) { - Row(L10n.Security.title) { - securityIcon.icon() - } - .rowArrow(isShowArrow) - } - .buttonStyle(.row) + if FeatureFlags.secure.faceID.valueOrFalse + || FeatureFlags.secure.lookscreen.valueOrFalse + || FeatureFlags.secure.CVVCodes.valueOrFalse + || FeatureFlags.secure.alertSecureCodes.valueOrFalse + || FeatureFlags.secure.blurMinimize.valueOrFalse + || FeatureFlags.secure.bruteForceSecure.valueOrFalse + || FeatureFlags.secure.photoBreaker.valueOrFalse + { + Row(L10n.Security.title) { + router.move(.security) + } leading: { + securityIcon.icon() } + .rowArrow() + } - if FeatureFlags.app.sounds.valueOrFalse || FeatureFlags.app.vibration.valueOrFalse { - NavigationLink(destination: SoundsAndVibrationsSettingsView() - ) { - Row(soundsAndVibrationTitle) { - FeatureFlags.app.sounds.valueOrFalse ? soundIcon.icon() : vibrationIcon.icon() - } - .rowArrow(isShowArrow) - } - .buttonStyle(.row) + if FeatureFlags.app.sounds.valueOrFalse || FeatureFlags.app.vibration.valueOrFalse { + Row(soundsAndVibrationTitle) { + router.move(.soundAndVibration) + } leading: { + FeatureFlags.app.sounds.valueOrFalse ? soundIcon.icon() : vibrationIcon.icon() } + .rowArrow() + } - if FeatureFlags.app.notifications.valueOrFalse { - NavigationLink(destination: NotificationsSettingsView() - ) { - Row(L10n.Settings.notifications) { - notificationsIcon.icon() - } - .rowArrow(isShowArrow) - } - .buttonStyle(.row) + if FeatureFlags.app.notifications.valueOrFalse { + Row(L10n.Settings.notifications) { + router.move(.notifications) + } leading: { + notificationsIcon.icon() } - - appSection + .rowArrow() } - } - } - var apperanceSettingsIcon: Image { - switch iconStyle { - case .line: - return Image.Design.paintingPalette - case .fill: - return Image.Design.PaintingPalette.fill - case .twoTone: - return Image.Design.PaintingPalette.twoTone + appSection } } + } - var cloudKitIcon: Image { - switch iconStyle { - case .line: - return Image.Weather.cloud2 - case .fill: - return Image.Weather.Cloud.Square.fill - case .twoTone: - return Image.Weather.Cloud.Square.twoTone - } + var apperanceSettingsIcon: Image { + switch iconStyle { + case .line: + return Image.Design.paintingPalette + case .fill: + return Image.Design.PaintingPalette.fill + case .twoTone: + return Image.Design.PaintingPalette.twoTone } + } - var securityIcon: Image { - switch iconStyle { - case .line: - return Image.Base.lock - case .fill: - return Image.Base.Lock.fill - case .twoTone: - return Image.Base.Lock.TwoTone.fill - } + var cloudKitIcon: Image { + switch iconStyle { + case .line: + return Image.Weather.cloud2 + case .fill: + return Image.Weather.Cloud.Square.fill + case .twoTone: + return Image.Weather.Cloud.Square.twoTone } + } - var soundIcon: Image { - switch iconStyle { - case .line: - return Image.Base.volumeUp - case .fill: - return Image.Base.VolumeUp.fill - case .twoTone: - return Image.Base.VolumeUp.TwoTone.fill - } + var securityIcon: Image { + switch iconStyle { + case .line: + return Image.Base.lock + case .fill: + return Image.Base.Lock.fill + case .twoTone: + return Image.Base.Lock.TwoTone.fill } + } - var vibrationIcon: Image { - switch iconStyle { - case .line: - return Image.Mobile.vibration - case .fill: - return Image.Mobile.Vibration.fill - case .twoTone: - return Image.Mobile.Vibration.twoTone - } + var soundIcon: Image { + switch iconStyle { + case .line: + return Image.Base.volumeUp + case .fill: + return Image.Base.VolumeUp.fill + case .twoTone: + return Image.Base.VolumeUp.TwoTone.fill } + } - var notificationsIcon: Image { - switch iconStyle { - case .line: - return Image.Base.notification - case .fill: - return Image.Base.Notification.fill - case .twoTone: - return Image.Base.Notification.TwoTone.fill - } + var vibrationIcon: Image { + switch iconStyle { + case .line: + return Image.Mobile.vibration + case .fill: + return Image.Mobile.Vibration.fill + case .twoTone: + return Image.Mobile.Vibration.twoTone } + } - // App Store Review - private var help: some View { - SectionView(L10n.Settings.supportSection) { - VStack(alignment: .leading) { - Row("Get help") { - isShowSupport.toggle() - } leading: { - helpIcon.icon() - } - .rowArrow(isShowArrow) - .buttonStyle(.row) - .sheet(isPresented: $isShowSupport) { - SupportView() - .presentationDetents([.medium]) - .presentationContentInteraction(.resizes) - .presentationCompactAdaptation(.sheet) - .scrollDisabled(true) - } + var notificationsIcon: Image { + switch iconStyle { + case .line: + return Image.Base.notification + case .fill: + return Image.Base.Notification.fill + case .twoTone: + return Image.Base.Notification.TwoTone.fill + } + } - Row("Send feedback") { - isShowFeedback.toggle() - } leading: { - chatIcon.icon() - } - .rowArrow(isShowArrow) - .buttonStyle(.row) - .sheet(isPresented: $isShowFeedback) { - FeedbackView() - .presentationDetents([.height(560)]) - .presentationContentInteraction(.resizes) - .presentationCompactAdaptation(.sheet) - .scrollDisabled(true) - } + // App Store Review + private var help: some View { + SectionView(L10n.Settings.supportSection) { + VStack(alignment: .leading) { + Row("Get help") { + #if os(iOS) + router.present(.support, detents: [.medium]) + #endif + } leading: { + helpIcon.icon() } + .rowArrow() + .buttonStyle(.row) + + Row("Send feedback") { + #if os(iOS) + router.present(.feedback, detents: [.medium]) + #endif + } leading: { + chatIcon.icon() + } + .rowArrow() + .buttonStyle(.row) } } + } - var heartIcon: Image { - switch iconStyle { - case .line: - return Image.Base.heart - case .fill: - return Image.Base.Heart.fill - case .twoTone: - return Image.Base.Heart.TwoTone.fill - } + var heartIcon: Image { + switch iconStyle { + case .line: + return Image.Base.heart + case .fill: + return Image.Base.Heart.fill + case .twoTone: + return Image.Base.Heart.TwoTone.fill } + } - var mailIcon: Image { - switch iconStyle { - case .line: - return Image.Base.message - case .fill: - return Image.Base.Message.fill - case .twoTone: - return Image.Base.Message.TwoTone.fill - } + var mailIcon: Image { + switch iconStyle { + case .line: + return Image.Base.message + case .fill: + return Image.Base.Message.fill + case .twoTone: + return Image.Base.Message.TwoTone.fill } + } - var chatIcon: Image { - switch iconStyle { - case .line: - return Image.Base.chat - case .fill: - return Image.Base.Chat.fill - case .twoTone: - return Image.Base.Chat.twoTone - } + var chatIcon: Image { + switch iconStyle { + case .line: + return Image.Base.chat + case .fill: + return Image.Base.Chat.fill + case .twoTone: + return Image.Base.Chat.twoTone } + } - var infoIcon: Image { - switch iconStyle { - case .line: - return Image.Base.Info.circle - case .fill: - return Image.Base.Info.Circle.fill - case .twoTone: - return Image.Base.Info.Circle.twoTone - } + var infoIcon: Image { + switch iconStyle { + case .line: + return Image.Base.Info.circle + case .fill: + return Image.Base.Info.Circle.fill + case .twoTone: + return Image.Base.Info.Circle.twoTone } + } - var oversizeIcon: Image { - switch iconStyle { - case .line: - return Image.Brands.oversize - case .fill: - return Image.Brands.Oversize.fill - case .twoTone: - return Image.Brands.Oversize.TwoTone.fill - } + var oversizeIcon: Image { + switch iconStyle { + case .line: + return Image.Brands.oversize + case .fill: + return Image.Brands.Oversize.fill + case .twoTone: + return Image.Brands.Oversize.TwoTone.fill } + } - var helpIcon: Image { - switch iconStyle { - case .line: - return Image.Alert.Help.circle - case .fill: - return Image.Alert.Help.Circle.fill - case .twoTone: - return Image.Alert.Help.Circle.twoTone - } + var helpIcon: Image { + switch iconStyle { + case .line: + return Image.Alert.Help.circle + case .fill: + return Image.Alert.Help.Circle.fill + case .twoTone: + return Image.Alert.Help.Circle.twoTone } + } - private var about: some View { - SectionView { - VStack(spacing: .zero) { - NavigationLink(destination: AboutView()) { - Row(L10n.Settings.about) { - infoIcon.icon() - } - .rowArrow(isShowArrow) - } - .buttonStyle(.row) + private var about: some View { + SectionView { + VStack(spacing: .zero) { + Row(L10n.Settings.about) { + router.move(.about) + } leading: { + infoIcon.icon() } + .rowArrow() } - } - - var soundsAndVibrationTitle: String { - if FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.soundsAndVibration - } else if FeatureFlags.app.sounds.valueOrFalse, !FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.sounds - } else if !FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.vibration - } else { - return "" - } - } - } -#endif -// Mac OS Settings -#if os(iOS) - extension SettingsView { - private var macSettings: some View { - VStack(alignment: .center, spacing: 0) { - Text("Mac") - } - .frame(width: 400, height: 300) - .navigationTitle(L10n.Settings.apperance) - .preferredColorScheme(theme.appearance.colorScheme) + .buttonStyle(.row) } } -#endif -#if os(iOS) - public extension SettingsView where HeadSection == EmptyView { - init(@ViewBuilder appSection: () -> AppSection) { - self.init(appSection: appSection, headSection: { EmptyView() }) + var soundsAndVibrationTitle: String { + if FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { + return L10n.Settings.soundsAndVibration + } else if FeatureFlags.app.sounds.valueOrFalse, !FeatureFlags.app.vibration.valueOrFalse { + return L10n.Settings.sounds + } else if !FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { + return L10n.Settings.vibration + } else { + return "" } } +} -#endif -#if os(iOS) - extension UINavigationController: UIGestureRecognizerDelegate { - override open func viewDidLoad() { - super.viewDidLoad() - interactivePopGestureRecognizer?.delegate = self +extension SettingsView { + private var macSettings: some View { + VStack(alignment: .center, spacing: 0) { + Text("Mac") } + .frame(width: 400, height: 300) + .navigationTitle(L10n.Settings.apperance) + .preferredColorScheme(theme.appearance.colorScheme) + } +} - public func gestureRecognizerShouldBegin(_: UIGestureRecognizer) -> Bool { - viewControllers.count > 1 - } +public extension SettingsView where HeadSection == EmptyView { + init(@ViewBuilder appSection: () -> AppSection) { + self.init(appSection: appSection, headSection: { EmptyView() }) } -#endif +} diff --git a/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift index ed0f81c..6465b3a 100644 --- a/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift @@ -9,84 +9,72 @@ import OversizeServices import OversizeUI import SwiftUI -// swiftlint:disable line_length -#if os(iOS) - public struct SoundsAndVibrationsSettingsView: View { - @Environment(\.verticalSizeClass) private var verticalSizeClass - @Environment(\.iconStyle) var iconStyle: IconStyle - @Environment(\.isPortrait) var isPortrait - @Environment(\.presentationMode) var presentationMode - @State var offset = CGPoint(x: 0, y: 0) - @StateObject var settingsService = SettingsService() +public struct SoundsAndVibrationsSettingsView: View { + @Environment(\.iconStyle) var iconStyle: IconStyle + @StateObject var settingsService = SettingsService() - public var body: some View { - PageView(title) { - iOSSettings - .surfaceContentRowMargins() - } - .leadingBar { - if !isPortrait, verticalSizeClass == .regular { - EmptyView() - } else { - BarButton(.back) - } - } - .backgroundSecondary() + public init() {} + + public var body: some View { + Page(title) { + iOSSettings + .surfaceContentRowMargins() } + .backgroundSecondary() + } - private var title: String { - if FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.soundsAndVibration - } else if FeatureFlags.app.sounds.valueOrFalse, !FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.sounds - } else if !FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.vibration - } else { - return "" - } + private var title: String { + if FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { + return L10n.Settings.soundsAndVibration + } else if FeatureFlags.app.sounds.valueOrFalse, !FeatureFlags.app.vibration.valueOrFalse { + return L10n.Settings.sounds + } else if !FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { + return L10n.Settings.vibration + } else { + return "" } } +} - extension SoundsAndVibrationsSettingsView { - private var iOSSettings: some View { - VStack(alignment: .center, spacing: 0) { - soundsAndVibrations - } +extension SoundsAndVibrationsSettingsView { + private var iOSSettings: some View { + VStack(alignment: .center, spacing: 0) { + soundsAndVibrations } } +} - extension SoundsAndVibrationsSettingsView { - private var soundsAndVibrations: some View { - SectionView { - VStack(spacing: .zero) { - if FeatureFlags.app.sounds.valueOrFalse { - Switch(isOn: $settingsService.soundsEnabled) { - Row(L10n.Settings.sounds) { - IconDeprecated(.music) - } +extension SoundsAndVibrationsSettingsView { + private var soundsAndVibrations: some View { + SectionView { + VStack(spacing: .zero) { + if FeatureFlags.app.sounds.valueOrFalse { + Switch(isOn: $settingsService.soundsEnabled) { + Row(L10n.Settings.sounds) { + IconDeprecated(.music) } } + } - if FeatureFlags.app.vibration.valueOrFalse { - Switch(isOn: $settingsService.vibrationEnabled) { - Row(L10n.Settings.vibration) { - vibrationIcon.icon() - } + if FeatureFlags.app.vibration.valueOrFalse { + Switch(isOn: $settingsService.vibrationEnabled) { + Row(L10n.Settings.vibration) { + vibrationIcon.icon() } } } } } + } - var vibrationIcon: Image { - switch iconStyle { - case .line: - return Image.Mobile.vibration - case .fill: - return Image.Mobile.Vibration.fill - case .twoTone: - return Image.Mobile.Vibration.twoTone - } + var vibrationIcon: Image { + switch iconStyle { + case .line: + return Image.Mobile.vibration + case .fill: + return Image.Mobile.Vibration.fill + case .twoTone: + return Image.Mobile.Vibration.twoTone } } -#endif +} diff --git a/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift index 59eab6b..6a42b66 100644 --- a/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift @@ -9,71 +9,65 @@ import OversizeUI import SwiftUI // swiftlint:disable line_length type_name -#if os(iOS) - public struct iCloudSettingsView: View { // Synchronization - @Environment(\.presentationMode) var presentationMode - @Environment(\.verticalSizeClass) private var verticalSizeClass - @Environment(\.isPortrait) var isPortrait - @StateObject var settingsService = SettingsService() - @State var offset = CGPoint(x: 0, y: 0) - public var body: some View { - PageView(L10n.Title.synchronization) { - iOSSettings - .surfaceContentRowMargins() - } - .leadingBar { - BarButton(.back) - } - .backgroundSecondary() +public struct iCloudSettingsView: View { // Synchronization + @StateObject var settingsService = SettingsService() + + public init() {} + + public var body: some View { + Page(L10n.Title.synchronization) { + iOSSettings + .surfaceContentRowMargins() } + .backgroundSecondary() } +} - extension iCloudSettingsView { - private var iOSSettings: some View { - VStack(alignment: .center, spacing: 0) { - soundsAndVibrations - } +extension iCloudSettingsView { + private var iOSSettings: some View { + VStack(alignment: .center, spacing: 0) { + soundsAndVibrations } } +} - extension iCloudSettingsView { - private var soundsAndVibrations: some View { - SectionView { - VStack(spacing: .zero) { - if FeatureFlags.app.сloudKit.valueOrFalse { - Switch(isOn: $settingsService.cloudKitEnabled) { - Row(L10n.Settings.iCloudSync) { - Image.Weather.Cloud.square.icon() - } - .premium() +extension iCloudSettingsView { + private var soundsAndVibrations: some View { + SectionView { + VStack(spacing: .zero) { + if FeatureFlags.app.сloudKit.valueOrFalse { + Switch(isOn: $settingsService.cloudKitEnabled) { + Row(L10n.Settings.iCloudSync) { + Image.Weather.Cloud.square.icon() } - .onPremiumTap() + .premium() } + .onPremiumTap() + } - if FeatureFlags.secure.CVVCodes.valueOrFalse { - Switch(isOn: $settingsService.cloudKitCVVEnabled) { - Row(L10n.Security.iCloudSyncCVVDescriptionCloudKit, - subtitle: settingsService.cloudKitCVVEnabled ? L10n.Security.iCloudSyncCVVDescriptionCloudKit : L10n.Security.iCloudSyncCVVDescriptionLocal) - { - Image.Security.cloudLock - .icon() - .frame(width: 24, height: 24) - } - .premium() - .onPremiumTap() + if FeatureFlags.secure.CVVCodes.valueOrFalse { + Switch(isOn: $settingsService.cloudKitCVVEnabled) { + Row(L10n.Security.iCloudSyncCVVDescriptionCloudKit, + subtitle: settingsService.cloudKitCVVEnabled ? L10n.Security.iCloudSyncCVVDescriptionCloudKit : L10n.Security.iCloudSyncCVVDescriptionLocal) + { + Image.Security.cloudLock + .icon() + .frame(width: 24, height: 24) } + .premium() + .onPremiumTap() } + } - if FeatureFlags.app.healthKit.valueOrFalse { - Switch(isOn: $settingsService.healthKitEnabled) { - Row("HealthKit synchronization", subtitle: "After switching on, data from the Health app will be downloaded") { - Image.Romantic.heart.icon() - } + if FeatureFlags.app.healthKit.valueOrFalse { + Switch(isOn: $settingsService.healthKitEnabled) { + Row("HealthKit synchronization", subtitle: "After switching on, data from the Health app will be downloaded") { + Image.Romantic.heart.icon() } } } } } } -#endif +} diff --git a/Sources/OversizeKit/StateKit/LoadingViewState.swift b/Sources/OversizeKit/StateKit/LoadingViewState.swift new file mode 100644 index 0000000..05176b8 --- /dev/null +++ b/Sources/OversizeKit/StateKit/LoadingViewState.swift @@ -0,0 +1,49 @@ +// +// Copyright © 2024 Alexander Romanov +// LoadingViewState.swift, created on 25.04.2024 +// + +import Foundation +import OversizeModels + +public enum LoadingViewState: Equatable { + case idle + case loading + case result(Result) + case error(AppError) +} + +public extension LoadingViewState { + var isLoading: Bool { + switch self { + case .loading, .idle: + return true + default: + return false + } + } + + var result: Result? { + switch self { + case let .result(result): + return result + default: + return nil + } + } + + static func == (lhs: LoadingViewState, rhs: LoadingViewState) -> Bool { + switch (lhs, rhs) { + case (.idle, .idle): + return true + case (.loading, .loading): + return true + case (.result, .result): + return true + case (.error, .error): + return true + default: + return false + } + } +} diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift b/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift index 3b18d10..1928817 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift @@ -57,7 +57,7 @@ } } - extension AppStoreProductViewController: SKStoreProductViewControllerDelegate { + extension AppStoreProductViewController: @preconcurrency SKStoreProductViewControllerDelegate { public func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { viewController.dismiss(animated: true) } diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstuctinsView.swift b/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstuctinsView.swift index a736466..b579acd 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstuctinsView.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstuctinsView.swift @@ -59,7 +59,7 @@ public struct StoreInstuctinsView: View { .environmentObject(viewModel) } } - .onChange(of: isPremium) { status in + .onChange(of: isPremium) { _, status in if status { dismiss() } @@ -80,12 +80,12 @@ public struct StoreInstuctinsView: View { VStack(spacing: .xSmall) { Text("How your free trial works") .largeTitle() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) if viewModel.isHaveSale { Group { Text("Begin your path towards feeling better with a ") - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) + Text("--% discount") .foregroundColor(.accent) @@ -107,7 +107,7 @@ public struct StoreInstuctinsView: View { .overlay { ScrollArrow(width: 30, offset: -5 + (offset * 0.05)) .stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round)) - .foregroundColor(.onSurfaceHighEmphasis.opacity(0.3)) + .foregroundColor(.onSurfacePrimary.opacity(0.3)) .frame(width: 30) .offset(y: screenSize.safeAreaHeight - 300) // .opacity(1 - (offset * 0.01)) @@ -128,12 +128,12 @@ public struct StoreInstuctinsView: View { VStack(spacing: .xSmall) { Text("How your free trial works") .largeTitle() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) if viewModel.isHaveSale { Group { Text("Begin your path towards feeling better with a ") - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) + Text("\(viewModel.saleProcent)% discount") .foregroundColor(.accent) @@ -155,7 +155,7 @@ public struct StoreInstuctinsView: View { .overlay { ScrollArrow(width: 30, offset: -5 + (offset * 0.05)) .stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round)) - .foregroundColor(.onSurfaceHighEmphasis.opacity(0.3)) + .foregroundColor(.onSurfacePrimary.opacity(0.3)) .frame(width: 30) .offset(y: screenSize.safeAreaHeight - 300) // .opacity(1 - (offset * 0.01)) @@ -179,7 +179,7 @@ public struct StoreInstuctinsView: View { await viewModel.updateSubscriptionStatus(products: data) } } - .onChange(of: data.purchasedAutoRenewable) { _ in + .onChange(of: data.purchasedAutoRenewable) { _, _ in Task { await viewModel.updateSubscriptionStatus(products: data) } @@ -192,7 +192,7 @@ public struct StoreInstuctinsView: View { HStack(alignment: .top, spacing: .small) { Resource.Store.zap .renderingMode(.template) - .foregroundColor(.onPrimaryHighEmphasis) + .foregroundColor(.onPrimary) .padding(.small) .background { Circle() @@ -226,7 +226,7 @@ public struct StoreInstuctinsView: View { HStack(alignment: .top, spacing: .small) { Image.Base.Notification.fill .renderingMode(.template) - .foregroundColor(Color.onSurfaceDisabled) + .foregroundColor(Color.onSurfaceTertiary) .padding(14) .background { Circle() @@ -252,7 +252,7 @@ public struct StoreInstuctinsView: View { HStack(alignment: .top, spacing: .small) { Image.Base.Star.fill .renderingMode(.template) - .foregroundColor(Color.onSurfaceDisabled) + .foregroundColor(Color.onSurfaceTertiary) .padding(14) .background { Circle() diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift b/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift index 2f522b6..e2c0f39 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift @@ -43,7 +43,7 @@ public struct StoreSpecialOfferView: View { } } - .onChange(of: isPremium) { status in + .onChange(of: isPremium) { _, status in if status { dismiss() } @@ -223,7 +223,7 @@ public struct StoreSpecialOfferView: View { .overlay { ScrollArrow(width: 30, offset: -5 + (offset * 0.05)) .stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round)) - .foregroundColor(.onSurfaceHighEmphasis.opacity(0.3)) + .foregroundColor(.onSurfacePrimary.opacity(0.3)) .frame(width: 30) .offset(y: screenSize.safeAreaHeight - 280) .opacity(1 - (offset * 0.01)) @@ -232,7 +232,7 @@ public struct StoreSpecialOfferView: View { VStack(spacing: .zero) { Text("Additional features in\nthe subscription") .title() - .onBackgroundHighEmphasisForegroundColor() + .onBackgroundPrimaryForeground() .multilineTextAlignment(.center) .fixedSize() .padding(.top, .large) @@ -252,7 +252,7 @@ public struct StoreSpecialOfferView: View { .task { await viewModel.updateSubscriptionStatus(products: data) } - .onChange(of: data.purchasedAutoRenewable) { _ in + .onChange(of: data.purchasedAutoRenewable) { _, _ in Task { await viewModel.updateSubscriptionStatus(products: data) } @@ -264,12 +264,12 @@ public struct StoreSpecialOfferView: View { VStack(spacing: .zero) { Text(badgeText.uppercased()) .footnote(.semibold) - .onBackgroundMediumEmphasisForegroundColor() + .onBackgroundSecondaryForeground() .padding(.bottom, .xxxSmall) Text(headline) .title(.bold) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) .frame(maxWidth: .infinity, alignment: .center) Text(event.title) @@ -277,7 +277,7 @@ public struct StoreSpecialOfferView: View { .foregroundColor(titleColor) Text(description) - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) .headline(.regular) .padding(.top, .xSmall) } @@ -332,7 +332,7 @@ public struct StoreSpecialOfferView: View { if let accentColor = event.accentColor { return Color(hex: accentColor) } else { - return Color.onBackgroundHighEmphasis + return Color.onBackgroundPrimary } } diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift b/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift index b7a9d9f..861a886 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift @@ -26,7 +26,7 @@ import SwiftUI } public var body: some View { - PageView { + Page { Group { switch viewModel.state { case .initial, .loading: @@ -39,22 +39,22 @@ import SwiftUI } .paddingContent(.horizontal) } - .backgroundLinerGradient(LinearGradient(colors: [.backgroundPrimary, .backgroundSecondary], startPoint: .top, endPoint: .center)) - .titleLabel { - PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium) - } - .leadingBar { - if !isPortrait, verticalSizeClass == .regular, isClosable { - EmptyView() - } else { - BarButton(.back) - } - } - .trailingBar { - if isClosable { - BarButton(.close) - } - } +// .backgroundLinerGradient(LinearGradient(colors: [.backgroundPrimary, .backgroundSecondary], startPoint: .top, endPoint: .center)) +// .titleLabel { +// PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium) +// } +// .leadingBar { +// if !isPortrait, verticalSizeClass == .regular, isClosable { +// EmptyView() +// } else { +// BarButton(.back) +// } +// } +// .trailingBar { +// if isClosable { +// BarButton(.close) +// } +// } .bottomToolbar(style: .none) { if !viewModel.isPremium { StorePaymentButtonBar() @@ -93,11 +93,11 @@ import SwiftUI VStack(spacing: .xxSmall) { Text(titleText) .title() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Text(subtitleText) .headline() - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) } .multilineTextAlignment(.center) @@ -120,11 +120,11 @@ import SwiftUI VStack(spacing: .xxSmall) { Text(titleText) .title() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Text(subtitleText) .headline() - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) } .multilineTextAlignment(.center) @@ -163,14 +163,14 @@ import SwiftUI await viewModel.updateSubscriptionStatus(products: data) } } - .onChange(of: data.purchasedAutoRenewable) { _ in + .onChange(of: data.purchasedAutoRenewable) { _, _ in Task { // When `purchasedSubscriptions` changes, get the latest subscription status. await viewModel.updateSubscriptionStatus(products: data) } } - .onChange(of: viewModel.isPremium) { newValue in - isShowFireworks = newValue + .onChange(of: viewModel.isPremium) { _, status in + isShowFireworks = status DispatchQueue.main.asyncAfter(deadline: .now() + 30) { isShowFireworks = false } diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift b/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift index 708b461..346fe95 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift @@ -23,7 +23,9 @@ public class StoreViewModel: ObservableObject { } @Injected(\.storeKitService) var storeKitService: StoreKitService - @Injected(\.localNotificationService) var localNotificationService: LocalNotificationServiceProtocol + #if !os(tvOS) + @Injected(\.localNotificationService) var localNotificationService: LocalNotificationServiceProtocol + #endif @Published var state = State.initial @@ -136,7 +138,6 @@ extension StoreViewModel { } } - // Percentage of decrease = |239.88 - 59.99|/239.88 = 179.89/239.88 = 0.74991662497916 = 74.991662497916% var saleProcent: String { guard let yearSubscriptionProduct else { return "" } if let monthSubscriptionProduct { @@ -303,23 +304,25 @@ extension StoreViewModel { } func addTrialNotification(product: Product) async { - if product.type == .autoRenewable, product.subscription?.introductoryOffer != nil { - do { - try await localNotificationService.requestAuthorization() - if let trialDaysCount = product.trialDaysCount { - let timeInterval = TimeInterval((trialDaysCount - 2) * 24 * 60 * 60) - let notificationTime = Date().addingTimeInterval(timeInterval) - let dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: notificationTime) - await localNotificationService.schedule(localNotification: .init( - id: UUID(), - title: "Trial ends soon", - body: "Subscription ends in 2 days", - dateComponents: dateComponents, - repeats: false - )) - } - } catch {} - } + #if !os(tvOS) + if product.type == .autoRenewable, product.subscription?.introductoryOffer != nil { + do { + try await localNotificationService.requestAuthorization() + if let trialDaysCount = product.trialDaysCount { + let timeInterval = TimeInterval((trialDaysCount - 2) * 24 * 60 * 60) + let notificationTime = Date().addingTimeInterval(timeInterval) + let dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: notificationTime) + await localNotificationService.schedule(localNotification: .init( + id: UUID(), + title: "Trial ends soon", + body: "Subscription ends in 2 days", + dateComponents: dateComponents, + repeats: false + )) + } + } catch {} + } + #endif } } diff --git a/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift b/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift index 0594af7..7fa7602 100644 --- a/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift +++ b/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift @@ -45,12 +45,12 @@ public struct PremiumBlockOverlay: ViewModifier { VStack(spacing: .small) { Text(title) .title() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) if let subtitle { Text(subtitle) .headline(.medium) - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) } } .multilineTextAlignment(.center) diff --git a/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift b/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift index 3922ccd..9e71cad 100644 --- a/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift +++ b/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift @@ -33,9 +33,11 @@ public struct PaymentButtonStyle: ButtonStyle { @Environment(\.isLoading) private var isLoading: Bool @Environment(\.isAccent) private var isAccent: Bool @Environment(\.elevation) private var elevation: Elevation - @Environment(\.controlSize) var controlSize: ControlSize @Environment(\.controlBorderShape) var controlBorderShape: ControlBorderShape @Environment(\.isBordered) var isBordered: Bool + #if !os(tvOS) + @Environment(\.controlSize) var controlSize: ControlSize + #endif private let isInfinityWidth: Bool? @@ -47,7 +49,7 @@ public struct PaymentButtonStyle: ButtonStyle { configuration.label .body(true) .opacity(isLoading ? 0 : 1) - .foregroundColor(.onPrimaryHighEmphasis) + .foregroundColor(.onPrimary) .padding(.horizontal, horizontalPadding) .padding(.vertical, verticalPadding) .frame(maxWidth: maxWidth) @@ -65,7 +67,7 @@ public struct PaymentButtonStyle: ButtonStyle { ) .overlay { RoundedRectangle(cornerRadius: 10, style: .continuous) - .strokeBorder(Color.onSurfaceHighEmphasis.opacity(0.15), lineWidth: 2) + .strokeBorder(Color.onSurfacePrimary.opacity(0.15), lineWidth: 2) .opacity(isBordered || theme.borderButtons ? 1 : 0) } } @@ -74,40 +76,48 @@ public struct PaymentButtonStyle: ButtonStyle { private func loadingView(for _: ButtonRole?) -> some View { if isLoading { ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: Color.onPrimaryHighEmphasis)) + .progressViewStyle(CircularProgressViewStyle(tint: Color.onPrimary)) } else { EmptyView() } } private var horizontalPadding: Space { - switch controlSize { - case .mini: - return .xxSmall - case .small: - return .small - case .regular: - return .small - case .large, .extraLarge: + #if os(tvOS) return .medium - @unknown default: - return .zero - } + #else + switch controlSize { + case .mini: + return .xxSmall + case .small: + return .small + case .regular: + return .small + case .large, .extraLarge: + return .medium + @unknown default: + return .zero + } + #endif } private var verticalPadding: Space { - switch controlSize { - case .mini: - return .xxSmall - case .small: - return .xxSmall - case .regular: - return .small - case .large, .extraLarge: + #if os(tvOS) return .medium - @unknown default: - return .zero - } + #else + switch controlSize { + case .mini: + return .xxSmall + case .small: + return .xxSmall + case .regular: + return .small + case .large, .extraLarge: + return .medium + @unknown default: + return .zero + } + #endif } private var backgroundOpacity: CGFloat { @@ -119,13 +129,17 @@ public struct PaymentButtonStyle: ButtonStyle { } private var maxWidth: CGFloat? { - if isInfinityWidth == nil, controlSize == .regular { - return .infinity - } else if let infinity = isInfinityWidth, infinity == true { - return .infinity - } else { + #if os(tvOS) return nil - } + #else + if isInfinityWidth == nil, controlSize == .regular { + return .infinity + } else if let infinity = isInfinityWidth, infinity == true { + return .infinity + } else { + return nil + } + #endif } } diff --git a/Sources/OversizeKit/StoreKit/Views/FireworksBubbles.swift b/Sources/OversizeKit/StoreKit/Views/FireworksBubbles.swift index c2e78af..6db793a 100644 --- a/Sources/OversizeKit/StoreKit/Views/FireworksBubbles.swift +++ b/Sources/OversizeKit/StoreKit/Views/FireworksBubbles.swift @@ -54,7 +54,7 @@ struct FireworksBubbles: View { ZStack { ForEach(0 ..< Int.random(in: 2 ... 5), id: \.self) { _ in Circle() - .fill(Color.onPrimaryDisabled) + .fill(Color.onPrimaryTertiary) .frame(width: 30, height: 30) .modifier(ParticlesBubblesModifier()) .offset(x: CGFloat.random(in: -200 ... 200), y: CGFloat.random(in: -200 ... 200)) diff --git a/Sources/OversizeKit/StoreKit/Views/PrmiumBannerRow.swift b/Sources/OversizeKit/StoreKit/Views/PrmiumBannerRow.swift index d479082..7818f76 100644 --- a/Sources/OversizeKit/StoreKit/Views/PrmiumBannerRow.swift +++ b/Sources/OversizeKit/StoreKit/Views/PrmiumBannerRow.swift @@ -67,14 +67,14 @@ public struct PrmiumBannerRow: View { Text(Info.store.subscriptionsName) .headline(.semibold) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Spacer() HStack(spacing: .small) { Text(viewModel.subsribtionStatusText) .headline(.medium) - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) Circle() .foregroundColor(viewModel.subsribtionStatusColor) @@ -118,13 +118,13 @@ public extension PrmiumBannerRow { } .background( RoundedRectangle(cornerRadius: Radius.small.rawValue, style: .continuous) - .fill(Color.onPrimaryHighEmphasis + .fill(Color.onPrimary )) Text(Info.store.subscriptionsDescription) .headline(.semibold) - .onPrimaryHighEmphasisForegroundColor() + .onPrimaryForeground() .multilineTextAlignment(.center) .padding(.top, Space.xSmall) .frame(maxWidth: 260) diff --git a/Sources/OversizeKit/StoreKit/Views/StoreFeatureDetailView.swift b/Sources/OversizeKit/StoreKit/Views/StoreFeatureDetailView.swift index d0ff026..d192629 100644 --- a/Sources/OversizeKit/StoreKit/Views/StoreFeatureDetailView.swift +++ b/Sources/OversizeKit/StoreKit/Views/StoreFeatureDetailView.swift @@ -45,13 +45,16 @@ public struct StoreFeatureDetailView: View { Button { dismiss() } label: { - IconDeprecated(.xMini, color: selection.screenURL != nil ? .onPrimaryHighEmphasis : .onSurfaceDisabled) - .padding(.xxSmall) - .background { - Circle() - .fill(.ultraThinMaterial) - } - .padding(.small) + IconDeprecated( + .xMini, + color: selection.screenURL != nil ? .onPrimary : .onSurfaceTertiary + ) + .padding(.xxSmall) + .background { + Circle() + .fill(.ultraThinMaterial) + } + .padding(.small) } } #endif diff --git a/Sources/OversizeKit/StoreKit/Views/StoreFeaturesLargeView.swift b/Sources/OversizeKit/StoreKit/Views/StoreFeaturesLargeView.swift index 0668344..3b20f80 100644 --- a/Sources/OversizeKit/StoreKit/Views/StoreFeaturesLargeView.swift +++ b/Sources/OversizeKit/StoreKit/Views/StoreFeaturesLargeView.swift @@ -54,11 +54,11 @@ struct StoreFeaturesLargeView: View { VStack(spacing: .xxSmall) { Text(feature.title.valueOrEmpty) .title2(.bold) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Text(feature.subtitle.valueOrEmpty) .body(.medium) - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) } .padding(.vertical, .medium) .padding(.horizontal, .xSmall) @@ -119,11 +119,11 @@ struct StoreFeaturesLargeView: View { VStack(spacing: .xSmall) { Text(feature.title.valueOrEmpty) .title2(.bold) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Text(feature.subtitle.valueOrEmpty) .body(.medium) - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) } } .padding(.vertical, .large) diff --git a/Sources/OversizeKit/StoreKit/Views/StoreFeaturesView.swift b/Sources/OversizeKit/StoreKit/Views/StoreFeaturesView.swift index a0c895b..c20fc1a 100644 --- a/Sources/OversizeKit/StoreKit/Views/StoreFeaturesView.swift +++ b/Sources/OversizeKit/StoreKit/Views/StoreFeaturesView.swift @@ -31,7 +31,7 @@ struct StoreFeaturesView: View { .renderingMode(.template) } } - .onPrimaryHighEmphasisForegroundColor() + .onPrimaryForeground() .iconOnSurface(surfaceSolor: backgroundColor(feature: feature)) } .rowArrow() diff --git a/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift b/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift index 0399d3b..42ad0df 100644 --- a/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift +++ b/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift @@ -23,7 +23,7 @@ struct StorePaymentButtonBar: View { if showDescription { Text(viewModel.selectedProductButtonDescription) .subheadline(.semibold) - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) .padding(.vertical, 20) } diff --git a/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift b/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift index 2fa0d08..a10a81c 100644 --- a/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift +++ b/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift @@ -148,7 +148,7 @@ public struct StoreProductView: View { if isSelected { Circle() - .fill(Color.onPrimaryHighEmphasis) + .fill(Color.onPrimary) .frame(width: 20, height: 20) .overlay { IconDeprecated(.checkMini, color: topLabelbackgroundColor) @@ -174,21 +174,21 @@ public struct StoreProductView: View { VStack(spacing: .zero) { Text(product.displayMonthsCount) .title2() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Text(product.displayMonthsCountDescription) .footnote(.bold) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) HStack(spacing: .zero) { Text(product.displayPrice) .subheadline(.semibold) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) .padding(.top, .xxxSmall) Text(product.displayPeriod) .caption2() - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) .padding(.top, .xxxSmall) } .padding(.top, .xxxSmall) @@ -209,7 +209,7 @@ public struct StoreProductView: View { .fill(topLabelbackgroundColor) .frame(width: 20, height: 20) .overlay { - IconDeprecated(.checkMini, color: Color.onPrimaryHighEmphasis) + IconDeprecated(.checkMini, color: Color.onPrimary) } .padding(.top, .xxxSmall) .padding(.trailing, .xxxSmall) @@ -230,12 +230,12 @@ public struct StoreProductView: View { HStack { Text(product.displayName) .headline() - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) if isHaveSale, !isPurchased { Text("Save " + saleProcent + "%") .caption2(.bold) - .foregroundColor(.onPrimaryHighEmphasis) + .foregroundColor(.onPrimary) .padding(.horizontal, .xxSmall) .padding(.vertical, .xxxSmall) .background { @@ -267,14 +267,14 @@ public struct StoreProductView: View { .caption2() .padding(.top, .xxxSmall) } - .foregroundColor(.onSurfaceDisabled) + .foregroundColor(.onSurfaceTertiary) .padding(.vertical, .xxSmall) #if os(iOS) if isHaveSale, !isPurchased { Text("Save " + saleProcent + "%") .caption2(.bold) - .foregroundColor(.onPrimaryHighEmphasis) + .foregroundColor(.onPrimary) .padding(.vertical, .xxxSmall) .frame(maxWidth: .infinity) .background { @@ -299,7 +299,7 @@ public struct StoreProductView: View { VStack(alignment: .trailing, spacing: .xxSmall) { Text(product.displayPriceWithPeriod) .headline(.semibold) - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) if let subscriptionUnit = product.subscription?.subscriptionPeriod.unit, subscriptionUnit == .year { HStack(spacing: 2) { @@ -307,12 +307,12 @@ public struct StoreProductView: View { Text(monthSubscriptionProduct.displayPrice) .strikethrough() .subheadline() - .foregroundColor(.onSurfaceDisabled) + .foregroundColor(.onSurfaceTertiary) } Text(product.displayMonthPrice + product.displayMonthPricePeriod) .subheadline() - .foregroundColor(.onSurfaceMediumEmphasis) + .foregroundColor(.onSurfaceSecondary) } } } @@ -379,7 +379,7 @@ public struct StoreProductView: View { var topLabelForegroundColor: Color { if isPurchased || isSelected { - return .onPrimaryHighEmphasis + return .onPrimary } else { return Palette.violet.color } @@ -387,7 +387,7 @@ public struct StoreProductView: View { var descriptionForegroundColor: Color { if isPurchased || product.type != .autoRenewable { - return .onSurfaceMediumEmphasis + return .onSurfaceSecondary } else { return .warning } diff --git a/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift b/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift index 692e91e..6b8ec96 100644 --- a/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift +++ b/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift @@ -21,11 +21,11 @@ struct SubscriptionPrivacyView: View { VStack(spacing: .xxSmall) { Text("About \(Info.store.subscriptionsName) subscription") .subheadline(.bold) - .foregroundColor(Color.onSurfaceDisabled) + .foregroundColor(Color.onSurfaceTertiary) Text("\(Info.store.subscriptionsName) subscription is required to get access to all functions. Regardless whether the subscription has free trial period or not it automatically renews with the price and duration given above unless it is canceled at least 24 hours before the end of the current period. Payment will be charged to your Apple ID account at the confirmation of purchase. Your account will be charged for renewal within 24 hours prior to the end of the current period. You can manage and cancel your subscriptions by going to your account settings on the App Store after purchase. Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable.") .caption() - .foregroundColor(Color.onSurfaceMediumEmphasis) + .foregroundColor(Color.onSurfaceSecondary) #if os(iOS) HStack(spacing: .xxSmall) { @@ -62,7 +62,7 @@ struct SubscriptionPrivacyView: View { } } .subheadline(.bold) - .foregroundColor(Color.onSurfaceDisabled) + .foregroundColor(Color.onSurfaceTertiary) .padding(.top, .xxxSmall) #endif } diff --git a/Sources/OversizeKit/SystemKit/SystemServices.swift b/Sources/OversizeKit/SystemKit/SystemServices.swift index 733abbf..a9cc75f 100644 --- a/Sources/OversizeKit/SystemKit/SystemServices.swift +++ b/Sources/OversizeKit/SystemKit/SystemServices.swift @@ -4,27 +4,25 @@ // import Factory -import OversizeCore -import OversizeLocalizable import OversizeServices import OversizeStoreService import OversizeUI import SwiftUI public struct SystemServicesModifier: ViewModifier { - @Injected(\.appStateService) var appState: AppStateService - @Injected(\.settingsService) var settingsService: SettingsServiceProtocol - @Injected(\.appStoreReviewService) var appStoreReviewService: AppStoreReviewServiceProtocol + @Injected(\.appStateService) private var appState: AppStateService + @Injected(\.settingsService) private var settingsService: SettingsServiceProtocol + @Injected(\.appStoreReviewService) private var appStoreReviewService: AppStoreReviewServiceProtocol - @Environment(\.scenePhase) var scenePhase: ScenePhase - @Environment(\.theme) var theme: ThemeSettings - @AppStorage("AppState.PremiumState") var isPremium: Bool = false + @Environment(\.scenePhase) private var scenePhase: ScenePhase + @Environment(\.theme) private var theme: ThemeSettings + @AppStorage("AppState.PremiumState") private var isPremium: Bool = false - @StateObject var hudState = HUDDeprecated() - @State var blurRadius: CGFloat = 0 - @State var oppacity: CGFloat = 1 + @State private var blurRadius: CGFloat = 0 + @State private var oppacity: CGFloat = 1 + @State private var screnSize: ScreenSize = .init(width: 375, height: 667) - enum FullScreenSheet: Identifiable, Equatable { + private enum FullScreenSheet: Identifiable, Equatable, Sendable { case onboarding case payWall case lockscreen @@ -33,53 +31,59 @@ public struct SystemServicesModifier: ViewModifier { } } - public init() {} + public nonisolated init() {} public func body(content: Content) -> some View { GeometryReader { geometry in content - .onChange(of: scenePhase, perform: { value in - switch value { - case .active: - if settingsService.blurMinimizeEnabend { - withAnimation { - blurRadius = 0 - } - } - case .background: - if settingsService.blurMinimizeEnabend { - withAnimation { - blurRadius = 10 - } - } - case .inactive: - if settingsService.blurMinimizeEnabend { - withAnimation { - blurRadius = 10 - } - } - @unknown default: - break - } - }) .blur(radius: blurRadius) .preferredColorScheme(theme.appearance.colorScheme) + .premiumStatus(isPremium) + .theme(ThemeSettings()) + .screenSize(screnSize) #if os(iOS) .accentColor(theme.accentColor) #endif - .premiumStatus(isPremium) - .theme(ThemeSettings()) - .screenSize(geometry) - .hudDeprecated(isPresented: $hudState.isPresented, type: $hudState.type) { - HUDContent(title: hudState.title, image: hudState.image, type: hudState.type) + .onAppear(perform: { onAppear(geometry: geometry) }) + .onChange(of: scenePhase) { _, phase in + onChangeScenePhase(phase) + } + } + } + + private func onChangeScenePhase(_ phase: ScenePhase) { + switch phase { + case .active: + if settingsService.blurMinimizeEnabend { + withAnimation { + blurRadius = 0 + } + } + case .background: + if settingsService.blurMinimizeEnabend { + withAnimation { + blurRadius = 10 } - .environmentObject(hudState) + } + case .inactive: + if settingsService.blurMinimizeEnabend { + withAnimation { + blurRadius = 10 + } + } + @unknown default: + break } } + + private func onAppear(geometry: GeometryProxy) { + let updatedScreenSize = ScreenSize(geometry: geometry) + screnSize = updatedScreenSize + } } public extension View { - func systemServices() -> some View { + nonisolated func systemServices() -> some View { modifier(SystemServicesModifier()) } } diff --git a/Sources/OversizeLocationKit/AddressField/AddressField.swift b/Sources/OversizeLocationKit/AddressField/AddressField.swift index 1a8faa4..2582a24 100644 --- a/Sources/OversizeLocationKit/AddressField/AddressField.swift +++ b/Sources/OversizeLocationKit/AddressField/AddressField.swift @@ -50,14 +50,16 @@ public struct AddressField: View { public var body: some View { Button { - isShowPicker.toggle() + #if !os(watchOS) + isShowPicker.toggle() + #endif } label: { VStack(alignment: .leading, spacing: .xSmall) { if fieldPlaceholderPosition == .adjacent { HStack { Text(title) .subheadline(.medium) - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) Spacer() } } @@ -68,7 +70,7 @@ public struct AddressField: View { Text(title) .font(!isSlectedAddress ? .headline : .subheadline) .fontWeight(!isSlectedAddress ? .medium : .semibold) - .onSurfaceDisabledForegroundColor() + .onSurfaceTertiaryForeground() .offset(y: !isSlectedAddress ? 0 : -13) .opacity(!isSlectedAddress ? 0 : 1) } @@ -79,16 +81,18 @@ public struct AddressField: View { .lineLimit(1) } Spacer() - IconDeprecated(.chevronDown, color: .onSurfaceHighEmphasis) + IconDeprecated(.chevronDown, color: .onSurfacePrimary) } } .contentShape(Rectangle()) } - .foregroundColor(.onSurfaceHighEmphasis) + .foregroundColor(.onSurfacePrimary) .buttonStyle(.field) - .sheet(isPresented: $isShowPicker) { - AddressPicker(address: $seletedAddress, location: $seletedLocation, place: $seletedPlace) - } + #if !os(watchOS) + .sheet(isPresented: $isShowPicker) { + AddressPicker(address: $seletedAddress, location: $seletedLocation, place: $seletedPlace) + } + #endif } private var fieldOffset: CGFloat { diff --git a/Sources/OversizeLocationKit/AddressPicker/AddressPicker.swift b/Sources/OversizeLocationKit/AddressPicker/AddressPicker.swift index 33b1775..c46b204 100644 --- a/Sources/OversizeLocationKit/AddressPicker/AddressPicker.swift +++ b/Sources/OversizeLocationKit/AddressPicker/AddressPicker.swift @@ -11,225 +11,227 @@ import OversizeLocationService import OversizeUI import SwiftUI -public struct AddressPicker: View { - @Environment(\.dismiss) private var dismiss - @StateObject private var viewModel = AddressPickerViewModel() - @FocusState private var isFocusSearth - - @Binding private var seletedAddress: String? - @Binding private var seletedLocation: CLLocationCoordinate2D? - @Binding private var seletedPlace: LocationAddress? - - public init( - address: Binding = .constant(nil), - location: Binding = .constant(nil), - place: Binding = .constant(nil) - ) { - _seletedAddress = address - _seletedLocation = location - _seletedPlace = place - } - - public var body: some View { - PageView("Location") { - LazyVStack(spacing: .zero) { - if viewModel.appError != nil { - currentLocation - } +#if !os(watchOS) + public struct AddressPicker: View { + @Environment(\.dismiss) private var dismiss + @StateObject private var viewModel = AddressPickerViewModel() + @FocusState private var isFocusSearth + + @Binding private var seletedAddress: String? + @Binding private var seletedLocation: CLLocationCoordinate2D? + @Binding private var seletedPlace: LocationAddress? + + public init( + address: Binding = .constant(nil), + location: Binding = .constant(nil), + place: Binding = .constant(nil) + ) { + _seletedAddress = address + _seletedLocation = location + _seletedPlace = place + } - if viewModel.searchTerm.isEmpty, !viewModel.lastSearchAddresses.isEmpty { - HStack(spacing: .zero) { - Text("Recent") - Spacer() + public var body: some View { + PageView("Location") { + LazyVStack(spacing: .zero) { + if viewModel.appError != nil { + currentLocation } - .title3() - .onSurfaceMediumEmphasisForegroundColor() - .padding(.vertical, .xxSmall) - .paddingContent(.horizontal) - recentResults - } else { - results + if viewModel.searchTerm.isEmpty, !viewModel.lastSearchAddresses.isEmpty { + HStack(spacing: .zero) { + Text("Recent") + Spacer() + } + .title3() + .onSurfaceSecondaryForeground() + .padding(.vertical, .xxSmall) + .paddingContent(.horizontal) + + recentResults + } else { + results + } } } - } - .leadingBar { - BarButton(.close) - } - .topToolbar { - TextField("Search places or addresses", text: $viewModel.searchTerm) - .submitScope(viewModel.searchTerm.count < 2) - .textFieldStyle(DefaultPlaceholderTextFieldStyle()) - .focused($isFocusSearth) - .submitLabel(.done) - .onSubmit { - if viewModel.searchTerm.count > 2 { - viewModel.isSaveFromSearth = true - seletedAddress = viewModel.searchTerm - Task { - let coordinate = try? await viewModel.locationService.fetchCoordinateFromAddress(viewModel.searchTerm) - if let coordinate { - let address = try? await viewModel.locationService.fetchAddressFromLocation(coordinate) - seletedLocation = coordinate - seletedPlace = address - } else { - seletedPlace = nil - seletedLocation = nil + .leadingBar { + BarButton(.close) + } + .topToolbar { + TextField("Search places or addresses", text: $viewModel.searchTerm) + .submitScope(viewModel.searchTerm.count < 2) + .textFieldStyle(.default) + .focused($isFocusSearth) + .submitLabel(.done) + .onSubmit { + if viewModel.searchTerm.count > 2 { + viewModel.isSaveFromSearth = true + seletedAddress = viewModel.searchTerm + Task { + let coordinate = try? await viewModel.locationService.fetchCoordinateFromAddress(viewModel.searchTerm) + if let coordinate { + let address = try? await viewModel.locationService.fetchAddressFromLocation(coordinate) + seletedLocation = coordinate + seletedPlace = address + } else { + seletedPlace = nil + seletedLocation = nil + } + viewModel.isSaveFromSearth = false + saveToHistory() + dismiss() } - viewModel.isSaveFromSearth = false - saveToHistory() - dismiss() } } - } - .overlay(alignment: .trailing) { - if viewModel.isSaveFromSearth { - ProgressView() - .padding(.trailing, .xSmall) + .overlay(alignment: .trailing) { + if viewModel.isSaveFromSearth { + ProgressView() + .padding(.trailing, .xSmall) + } } - } - } - // .scrollDismissesKeyboard(.immediately) - .task(priority: .background) { - do { - try await viewModel.updateCurrentPosition() - if viewModel.isSaveCurentPositon { - onSaveCurrntPosition() - } - } catch {} - } - .onAppear { - isFocusSearth = true - } - } - - private var currentLocation: some View { - Row("Current Location") { - if viewModel.isFetchUpdatePositon { - viewModel.isSaveCurentPositon = true - } else { - onSaveCurrntPosition() } - } leading: { - IconDeprecated(.navigation) - .iconOnSurface() + // .scrollDismissesKeyboard(.immediately) + .task(priority: .background) { + do { + try await viewModel.updateCurrentPosition() + if viewModel.isSaveCurentPositon { + onSaveCurrntPosition() + } + } catch {} + } + .onAppear { + isFocusSearth = true + } } - .padding(.bottom, viewModel.searchTerm.isEmpty ? .small : .zero) - .loading(viewModel.isSaveCurentPositon) - } - - private var recentResults: some View { - ForEach(viewModel.lastSearchAddresses.reversed()) { address in - Row(address.address ?? address.place?.address ?? "Latitude: \(address.location?.latitude ?? 0), longitude:longitude \(address.location?.longitude ?? 0)") { - if let latitude = address.location?.latitude, let longitude = address.location?.longitude { - onCompleteSearth(seletedAddress: address.address, seletedLocation: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), seletedPlace: address.place, saveToHistory: false) + private var currentLocation: some View { + Row("Current Location") { + if viewModel.isFetchUpdatePositon { + viewModel.isSaveCurentPositon = true } else { - onCompleteSearth(seletedAddress: address.address, seletedLocation: nil, seletedPlace: address.place, saveToHistory: false) + onSaveCurrntPosition() } } leading: { - IconDeprecated(.mapPin) + IconDeprecated(.navigation) .iconOnSurface() } - .rowClearButton { - if let fooOffset = viewModel.lastSearchAddresses.firstIndex(where: { $0.id == address.id }) { - viewModel.lastSearchAddresses.remove(at: fooOffset) + .padding(.bottom, viewModel.searchTerm.isEmpty ? .small : .zero) + .loading(viewModel.isSaveCurentPositon) + } + + private var recentResults: some View { + ForEach(viewModel.lastSearchAddresses.reversed()) { address in + + Row(address.address ?? address.place?.address ?? "Latitude: \(address.location?.latitude ?? 0), longitude:longitude \(address.location?.longitude ?? 0)") { + if let latitude = address.location?.latitude, let longitude = address.location?.longitude { + onCompleteSearth(seletedAddress: address.address, seletedLocation: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), seletedPlace: address.place, saveToHistory: false) + } else { + onCompleteSearth(seletedAddress: address.address, seletedLocation: nil, seletedPlace: address.place, saveToHistory: false) + } + } leading: { + IconDeprecated(.mapPin) + .iconOnSurface() + } + .rowClearButton { + if let fooOffset = viewModel.lastSearchAddresses.firstIndex(where: { $0.id == address.id }) { + viewModel.lastSearchAddresses.remove(at: fooOffset) + } } } } - } - private var results: some View { - ForEach(viewModel.locationResults, id: \.self) { location in + private var results: some View { + ForEach(viewModel.locationResults, id: \.self) { location in - Row(location.title, subtitle: location.subtitle) { - reverseGeo(location: location) - } leading: { - IconDeprecated(.mapPin) - .iconOnSurface() + Row(location.title, subtitle: location.subtitle) { + reverseGeo(location: location) + } leading: { + IconDeprecated(.mapPin) + .iconOnSurface() + } } } - } - func reverseGeo(location: MKLocalSearchCompletion) { - let searchRequest = MKLocalSearch.Request(completion: location) - let search = MKLocalSearch(request: searchRequest) - var coordinateK: CLLocationCoordinate2D? - search.start { response, error in - if error == nil, let coordinate = response?.mapItems.first?.placemark.coordinate { - coordinateK = coordinate - } + func reverseGeo(location: MKLocalSearchCompletion) { + let searchRequest = MKLocalSearch.Request(completion: location) + let search = MKLocalSearch(request: searchRequest) + var coordinateK: CLLocationCoordinate2D? + search.start { response, error in + if error == nil, let coordinate = response?.mapItems.first?.placemark.coordinate { + coordinateK = coordinate + } - if let c = coordinateK { - let location = CLLocation(latitude: c.latitude, longitude: c.longitude) - CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in + if let c = coordinateK { + let location = CLLocation(latitude: c.latitude, longitude: c.longitude) + CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in - guard let placemark = placemarks?.first else { - let errorString = error?.localizedDescription ?? "Unexpected Error" - print("Unable to reverse geocode, \(errorString)") - return - } + guard let placemark = placemarks?.first else { + let errorString = error?.localizedDescription ?? "Unexpected Error" + print("Unable to reverse geocode, \(errorString)") + return + } - let reversedGeoLocation = LocationAddress(with: placemark) + let reversedGeoLocation = LocationAddress(with: placemark) - let address = "\(reversedGeoLocation.streetName) \(reversedGeoLocation.streetNumber)".capitalizingFirstLetter() - onCompleteSearth(seletedAddress: address, seletedLocation: c, seletedPlace: reversedGeoLocation) + let address = "\(reversedGeoLocation.streetName) \(reversedGeoLocation.streetNumber)".capitalizingFirstLetter() + onCompleteSearth(seletedAddress: address, seletedLocation: c, seletedPlace: reversedGeoLocation) + } } } } - } - func onCompleteSearth(seletedAddress: String?, seletedLocation: CLLocationCoordinate2D?, seletedPlace: LocationAddress?, saveToHistory: Bool = true) { - if let seletedAddress { - self.seletedAddress = seletedAddress - } else { - self.seletedAddress = seletedPlace?.address + func onCompleteSearth(seletedAddress: String?, seletedLocation: CLLocationCoordinate2D?, seletedPlace: LocationAddress?, saveToHistory: Bool = true) { + if let seletedAddress { + self.seletedAddress = seletedAddress + } else { + self.seletedAddress = seletedPlace?.address + } + self.seletedLocation = seletedLocation + self.seletedPlace = seletedPlace + if saveToHistory { + self.saveToHistory() + } + dismiss() } - self.seletedLocation = seletedLocation - self.seletedPlace = seletedPlace - if saveToHistory { - self.saveToHistory() + + private func onSaveCurrntPosition() { + Task { + let address = try? await viewModel.locationService.fetchAddressFromLocation(viewModel.currentLocation) + if let address { + seletedAddress = address.address + seletedPlace = address + } else { + seletedAddress = nil + seletedPlace = nil + } + seletedLocation = viewModel.currentLocation + saveToHistory() + dismiss() + } } - dismiss() - } - private func onSaveCurrntPosition() { - Task { - let address = try? await viewModel.locationService.fetchAddressFromLocation(viewModel.currentLocation) - if let address { - seletedAddress = address.address - seletedPlace = address + func saveToHistory() { + let lastSearth: SearchHistoryAddress + if let seletedLocation { + lastSearth = SearchHistoryAddress( + id: UUID().uuidString, + address: seletedAddress, + location: SearchHistoryLocationCoordinate(coordinate: seletedLocation), + place: seletedPlace + ) } else { - seletedAddress = nil - seletedPlace = nil + lastSearth = SearchHistoryAddress( + id: UUID().uuidString, + address: seletedAddress, + location: nil, + place: seletedPlace + ) } - seletedLocation = viewModel.currentLocation - saveToHistory() - dismiss() + viewModel.lastSearchAddresses.append(lastSearth) } - } - func saveToHistory() { - let lastSearth: SearchHistoryAddress - if let seletedLocation { - lastSearth = SearchHistoryAddress( - id: UUID().uuidString, - address: seletedAddress, - location: SearchHistoryLocationCoordinate(coordinate: seletedLocation), - place: seletedPlace - ) - } else { - lastSearth = SearchHistoryAddress( - id: UUID().uuidString, - address: seletedAddress, - location: nil, - place: seletedPlace - ) + private func onDoneAction() { + dismiss() } - viewModel.lastSearchAddresses.append(lastSearth) - } - - private func onDoneAction() { - dismiss() } -} +#endif diff --git a/Sources/OversizeLocationKit/AddressPicker/AddressPickerViewModel.swift b/Sources/OversizeLocationKit/AddressPicker/AddressPickerViewModel.swift index 92e6fd5..6db6c07 100644 --- a/Sources/OversizeLocationKit/AddressPicker/AddressPickerViewModel.swift +++ b/Sources/OversizeLocationKit/AddressPicker/AddressPickerViewModel.swift @@ -11,75 +11,77 @@ import OversizeLocationService import OversizeModels import SwiftUI -@MainActor -class AddressPickerViewModel: NSObject, ObservableObject { - @Injected(\.locationService) var locationService: LocationServiceProtocol +#if !os(watchOS) + @MainActor + class AddressPickerViewModel: NSObject, ObservableObject { + @Injected(\.locationService) var locationService: LocationServiceProtocol - @Published var locationResults: [MKLocalSearchCompletion] = .init() - @Published var searchTerm: String = .init() - @AppStorage("AppState.LastSearchAddresses") var lastSearchAddresses: [SearchHistoryAddress] = .init() + @Published var locationResults: [MKLocalSearchCompletion] = .init() + @Published var searchTerm: String = .init() + @AppStorage("AppState.LastSearchAddresses") var lastSearchAddresses: [SearchHistoryAddress] = .init() - @Published var currentLocation: CLLocationCoordinate2D = .init(latitude: 0, longitude: 0) + @Published var currentLocation: CLLocationCoordinate2D = .init(latitude: 0, longitude: 0) - @Published var isFetchUpdatePositon: Bool = .init(false) - @Published var isSaveCurentPositon: Bool = .init(false) - @Published var isSaveFromSearth: Bool = .init(false) + @Published var isFetchUpdatePositon: Bool = .init(false) + @Published var isSaveCurentPositon: Bool = .init(false) + @Published var isSaveFromSearth: Bool = .init(false) - private var cancellables: Set = [] + private var cancellables: Set = [] - private var searchCompleter = MKLocalSearchCompleter() - private var currentPromise: ((Result<[MKLocalSearchCompletion], Error>) -> Void)? + private var searchCompleter = MKLocalSearchCompleter() + private var currentPromise: ((Result<[MKLocalSearchCompletion], Error>) -> Void)? - @State var appError: AppError? + @State var appError: AppError? - override init() { - super.init() - searchCompleter.delegate = self - searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address]) + override init() { + super.init() + searchCompleter.delegate = self + searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address]) - $searchTerm - .debounce(for: .seconds(0.2), scheduler: RunLoop.main) - .removeDuplicates() - .flatMap { currentSearchTerm in - self.searchTermToResults(searchTerm: currentSearchTerm) + $searchTerm + .debounce(for: .seconds(0.2), scheduler: RunLoop.main) + .removeDuplicates() + .flatMap { currentSearchTerm in + self.searchTermToResults(searchTerm: currentSearchTerm) + } + .sink(receiveCompletion: { _ in + // Handle error + }, receiveValue: { results in + self.locationResults = results + }) + .store(in: &cancellables) + } + + func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> { + Future { promise in + self.searchCompleter.queryFragment = searchTerm + self.currentPromise = promise } - .sink(receiveCompletion: { _ in - // Handle error - }, receiveValue: { results in - self.locationResults = results - }) - .store(in: &cancellables) + } } - func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> { - Future { promise in - self.searchCompleter.queryFragment = searchTerm - self.currentPromise = promise + extension AddressPickerViewModel: @preconcurrency MKLocalSearchCompleterDelegate { + func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { + currentPromise?(.success(completer.results)) } - } -} -extension AddressPickerViewModel: MKLocalSearchCompleterDelegate { - func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { - currentPromise?(.success(completer.results)) + func completer(_: MKLocalSearchCompleter, didFailWithError _: Error) {} } - func completer(_: MKLocalSearchCompleter, didFailWithError _: Error) {} -} - -extension AddressPickerViewModel { - func updateCurrentPosition() async throws { - let status = locationService.permissionsStatus() - switch status { - case .success: - isFetchUpdatePositon = true - let currentPosition = try await locationService.currentLocation() - guard let newLocation = currentPosition else { return } - currentLocation = newLocation - print("📍 Location: \(newLocation.latitude), \(newLocation.longitude)") - isFetchUpdatePositon = false - case let .failure(error): - appError = error + extension AddressPickerViewModel { + func updateCurrentPosition() async throws { + let status = locationService.permissionsStatus() + switch status { + case .success: + isFetchUpdatePositon = true + let currentPosition = try await locationService.currentLocation() + guard let newLocation = currentPosition else { return } + currentLocation = newLocation + print("📍 Location: \(newLocation.latitude), \(newLocation.longitude)") + isFetchUpdatePositon = false + case let .failure(error): + appError = error + } } } -} +#endif diff --git a/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift b/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift index 93ed613..e36b40d 100644 --- a/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift +++ b/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift @@ -7,152 +7,156 @@ import MapKit import OversizeUI import SwiftUI -public struct MapCoordinateView: View { - @Environment(\.screenSize) var screenSize - @Environment(\.openURL) var openURL - @StateObject var viewModel: MapCoordinateViewModel +#if !os(watchOS) + public struct MapCoordinateView: View { + @Environment(\.screenSize) var screenSize + @Environment(\.openURL) var openURL + @StateObject var viewModel: MapCoordinateViewModel - public init(_ location: CLLocationCoordinate2D, annotation: String? = nil) { - _viewModel = StateObject(wrappedValue: MapCoordinateViewModel(location: location, annotation: annotation)) - } + public init(_ location: CLLocationCoordinate2D, annotation: String? = nil) { + _viewModel = StateObject(wrappedValue: MapCoordinateViewModel(location: location, annotation: annotation)) + } - public var body: some View { - VStack(spacing: 0) { - if #available(iOS 16.0, *) { - mapView - .ignoresSafeArea() - .safeAreaInset(edge: .top) { - ModalNavigationBar(title: viewModel.annotation ?? "", largeTitle: false, leadingBar: { - BarButton(.back) - }, trailingBar: { - BarButton(.icon(.map, action: { - viewModel.isShowRoutePickerSheet.toggle() - })) - }) - .background(.thickMaterial, ignoresSafeAreaEdges: .top) - } - #if os(iOS) - .toolbar(.hidden, for: .tabBar) - #endif - } else { - mapView - .safeAreaInset(edge: .top) { - ModalNavigationBar(title: viewModel.annotation ?? "", largeTitle: false, leadingBar: { - BarButton(.back) - }, trailingBar: { - BarButton(.icon(.map, action: { - viewModel.isShowRoutePickerSheet.toggle() - })) - }) - } + public var body: some View { + VStack(spacing: 0) { + if #available(iOS 16.0, *) { + mapView + .ignoresSafeArea() + .safeAreaInset(edge: .top) { + ModalNavigationBar(title: viewModel.annotation ?? "", largeTitle: false, leadingBar: { + BarButton(.back) + }, trailingBar: { + BarButton(.icon(.map, action: { + viewModel.isShowRoutePickerSheet.toggle() + })) + }) + .background(.thickMaterial, ignoresSafeAreaEdges: .top) + } + #if os(iOS) + .toolbar(.hidden, for: .tabBar) + #endif + } else { + mapView + .safeAreaInset(edge: .top) { + ModalNavigationBar(title: viewModel.annotation ?? "", largeTitle: false, leadingBar: { + BarButton(.back) + }, trailingBar: { + BarButton(.icon(.map, action: { + viewModel.isShowRoutePickerSheet.toggle() + })) + }) + } + } + } + .sheet(isPresented: $viewModel.isShowRoutePickerSheet) { + routeSheetView + .presentationDetents([.height(260)]) } } - .sheet(isPresented: $viewModel.isShowRoutePickerSheet) { - routeSheetView - .presentationDetents([.height(260)]) - } - } - var mapView: some View { - ZStack(alignment: .trailing) { - Map(coordinateRegion: region, showsUserLocation: true, userTrackingMode: $viewModel.userTrackingMode, annotationItems: viewModel.annotations) { - MapMarker(coordinate: $0.coordinate) + var mapView: some View { + ZStack(alignment: .trailing) { + Map(coordinateRegion: region, showsUserLocation: true, userTrackingMode: $viewModel.userTrackingMode, annotationItems: viewModel.annotations) { + MapMarker(coordinate: $0.coordinate) + } + controlButtons } - controlButtons } - } - private var region: Binding { - Binding { - viewModel.region - } set: { region in - DispatchQueue.main.async { - viewModel.region = region + private var region: Binding { + Binding { + viewModel.region + } set: { region in + DispatchQueue.main.async { + viewModel.region = region + } } } - } - var controlButtons: some View { - VStack { - Spacer() - VStack(spacing: .zero) { - Button { - viewModel.zoomIn() - } label: { - IconDeprecated(.plus) - .onSurfaceMediumEmphasisForegroundColor() - .padding(.xxSmall) - } + var controlButtons: some View { + VStack { + Spacer() + VStack(spacing: .zero) { + Button { + viewModel.zoomIn() + } label: { + IconDeprecated(.plus) + .onSurfaceSecondaryForeground() + .padding(.xxSmall) + } + Button { + viewModel.zoomOut() + } label: { + IconDeprecated(.minus) + .onSurfaceSecondaryForeground() + .padding(.xxSmall) + } + } + .background { + Capsule() + .fillSurfacePrimary() + .shadowElevaton(.z1) + } + Spacer() + } + .overlay(alignment: .bottomTrailing, content: { Button { - viewModel.zoomOut() + viewModel.positionInLocation() + } label: { - IconDeprecated(.minus) - .onSurfaceMediumEmphasisForegroundColor() + IconDeprecated(.navigation) + .onSurfaceSecondaryForeground() .padding(.xxSmall) } - } - .background { - Capsule() - .fillSurfacePrimary() - .shadowElevaton(.z1) - } - Spacer() + .background { + Capsule() + .fillSurfacePrimary() + .shadowElevaton(.z1) + } + }) + .padding(.trailing, 16) + .padding(.bottom, screenSize.safeAreaBottom) } - .overlay(alignment: .bottomTrailing, content: { - Button { - viewModel.positionInLocation() - - } label: { - IconDeprecated(.navigation) - .onSurfaceMediumEmphasisForegroundColor() - .padding(.xxSmall) - } - .background { - Capsule() - .fillSurfacePrimary() - .shadowElevaton(.z1) - } - }) - .padding(.trailing, 16) - .padding(.bottom, screenSize.safeAreaBottom) - } - var routeSheetView: some View { - PageView("Route") { - SectionView { - Row("Apple Maps") { - onTapAppleMaps() - } - Row("Google Maps") { - onTapGoogleMaps() + var routeSheetView: some View { + PageView("Route") { + SectionView { + Row("Apple Maps") { + onTapAppleMaps() + } + Row("Google Maps") { + onTapGoogleMaps() + } } } + .leadingBar(leadingBar: { + BarButton(.close) + }) + .backgroundSecondary() + .disableScrollShadow(true) + .surfaceContentRowMargins() } - .leadingBar(leadingBar: { - BarButton(.close) - }) - .backgroundSecondary() - .disableScrollShadow(true) - .surfaceContentRowMargins() - } - func onTapAppleMaps() { - let placemark = MKPlacemark(coordinate: viewModel.location, addressDictionary: nil) - let mapItem = MKMapItem(placemark: placemark) - mapItem.name = viewModel.annotation - mapItem.openInMaps() - viewModel.isShowRoutePickerSheet.toggle() - } + func onTapAppleMaps() { + #if !os(tvOS) + let placemark = MKPlacemark(coordinate: viewModel.location, addressDictionary: nil) + let mapItem = MKMapItem(placemark: placemark) + mapItem.name = viewModel.annotation + mapItem.openInMaps() + viewModel.isShowRoutePickerSheet.toggle() + #endif + } - func onTapGoogleMaps() { - guard let url = URL(string: "comgooglemaps://?saddr=\(viewModel.location.latitude),\(viewModel.location.longitude)") else { return } - openURL(url) + func onTapGoogleMaps() { + guard let url = URL(string: "comgooglemaps://?saddr=\(viewModel.location.latitude),\(viewModel.location.longitude)") else { return } + openURL(url) + } } -} -struct MapCoordinateView_Previews: PreviewProvider { - static var previews: some View { - MapCoordinateView(.init(latitude: 100, longitude: 100)) + struct MapCoordinateView_Previews: PreviewProvider { + static var previews: some View { + MapCoordinateView(.init(latitude: 100, longitude: 100)) + } } -} +#endif diff --git a/Sources/OversizeNotificationKit/LocalNotificationSetScreenViewModel.swift b/Sources/OversizeNotificationKit/LocalNotificationSetScreenViewModel.swift index be6e46c..94d24ed 100644 --- a/Sources/OversizeNotificationKit/LocalNotificationSetScreenViewModel.swift +++ b/Sources/OversizeNotificationKit/LocalNotificationSetScreenViewModel.swift @@ -9,68 +9,77 @@ import OversizeModels import OversizeNotificationService import SwiftUI -@MainActor -class LocalNotificationSetScreenViewModel: ObservableObject { - @Injected(\.localNotificationService) var localNotificationService: LocalNotificationServiceProtocol - @Published var state = State.initial +#if !os(tvOS) + @MainActor + class LocalNotificationSetScreenViewModel: ObservableObject { + @Injected(\.localNotificationService) var localNotificationService: LocalNotificationServiceProtocol + @Published var state = State.initial - public let id: UUID - private let date: Date - private let title: String - private let body: String - private let userInfo: [AnyHashable: Any]? + public let id: UUID + private let date: Date + private let title: String + private let body: String + private let userInfo: [AnyHashable: Any]? - init( - id: UUID, - date: Date, - title: String, - body: String, - userInfo: [AnyHashable: Any]? = nil - ) { - self.id = id - self.date = date - self.title = title - self.body = body - self.userInfo = userInfo - } + init( + id: UUID, + date: Date, + title: String, + body: String, + userInfo: [AnyHashable: Any]? = nil + ) { + self.id = id + self.date = date + self.title = title + self.body = body + self.userInfo = userInfo + } - func setNotification(timeBefore: LocalNotificationTime) async { - let notificationTime = date.addingTimeInterval(timeBefore.timeInterval) - let dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: notificationTime) - await localNotificationService.schedule(localNotification: .init( - id: id, - title: title, - body: body, - dateComponents: dateComponents, - repeats: false, - userInfo: userInfo - )) - } + func setNotification(timeBefore: LocalNotificationTime) async { + let notificationTime = date.addingTimeInterval(timeBefore.timeInterval) + let dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: notificationTime) - func fetchPandingNotification() async -> Bool { - let ids = await localNotificationService.fetchPendingIds() - return ids.contains(id.uuidString) - } + let stringUserInfo = userInfo?.reduce(into: [String: String]()) { result, pair in + if let key = pair.key as? String, let value = pair.value as? String { + result[key] = value + } + } - func deleteNotification() { - localNotificationService.removeRequest(withIdentifier: id.uuidString) - } + await localNotificationService.schedule(localNotification: .init( + id: id, + title: title, + body: body, + dateComponents: dateComponents, + repeats: false, + userInfo: stringUserInfo + )) + } - func requestAccsess() async { - let result = await localNotificationService.requestAccess() - switch result { - case .success: - state = .result - case let .failure(error): - state = .error(error) + func fetchPandingNotification() async -> Bool { + let ids = await localNotificationService.fetchPendingIds() + return ids.contains(id.uuidString) + } + + func deleteNotification() { + localNotificationService.removeRequest(withIdentifier: id.uuidString) + } + + func requestAccsess() async { + let result = await localNotificationService.requestAccess() + switch result { + case .success: + state = .result + case let .failure(error): + state = .error(error) + } } } -} -extension LocalNotificationSetScreenViewModel { - enum State { - case initial - case result - case error(AppError) + extension LocalNotificationSetScreenViewModel { + enum State { + case initial + case result + case error(AppError) + } } -} +#endif diff --git a/Sources/OversizeNotificationKit/LocalNotificationView.swift b/Sources/OversizeNotificationKit/LocalNotificationView.swift index 87cbe51..e574636 100644 --- a/Sources/OversizeNotificationKit/LocalNotificationView.swift +++ b/Sources/OversizeNotificationKit/LocalNotificationView.swift @@ -9,106 +9,108 @@ import OversizeUI import SwiftUI import UserNotifications -public struct LocalNotificationView: View { - @Environment(\.dismiss) var dismiss - @Binding private var selection: LocalNotificationTime - @State private var isPendingNotification: Bool = false - @StateObject var viewModel: LocalNotificationSetScreenViewModel - private let saveAction: ((UUID?) -> Void)? +#if !os(tvOS) + public struct LocalNotificationView: View { + @Environment(\.dismiss) var dismiss + @Binding private var selection: LocalNotificationTime + @State private var isPendingNotification: Bool = false + @StateObject var viewModel: LocalNotificationSetScreenViewModel + private let saveAction: ((UUID?) -> Void)? - public init( - _ selection: Binding, - id: UUID, - title: String, - body: String, - date: Date, - userInfo: [AnyHashable: Any]? = nil, - saveAction: ((UUID?) -> Void)? = nil - ) { - _viewModel = StateObject(wrappedValue: LocalNotificationSetScreenViewModel( - id: id, - date: date, - title: title, - body: body, - userInfo: userInfo - )) - _selection = selection - self.saveAction = saveAction - } + public init( + _ selection: Binding, + id: UUID, + title: String, + body: String, + date: Date, + userInfo: [AnyHashable: Any]? = nil, + saveAction: ((UUID?) -> Void)? = nil + ) { + _viewModel = StateObject(wrappedValue: LocalNotificationSetScreenViewModel( + id: id, + date: date, + title: title, + body: body, + userInfo: userInfo + )) + _selection = selection + self.saveAction = saveAction + } - public var body: some View { - switch viewModel.state { - case .initial: - contnent - .task { - await viewModel.requestAccsess() - } - case .result: - contnent - .task { - let pendingStatus = await viewModel.fetchPandingNotification() - if pendingStatus { - isPendingNotification = true + public var body: some View { + switch viewModel.state { + case .initial: + contnent + .task { + await viewModel.requestAccsess() } + case .result: + contnent + .task { + let pendingStatus = await viewModel.fetchPandingNotification() + if pendingStatus { + isPendingNotification = true + } + } + case let .error(error): + PageView("Notification") { + ErrorView(error) + } + .leadingBar { + BarButton(.close) } - case let .error(error): - PageView("Notification") { - ErrorView(error) - } - .leadingBar { - BarButton(.close) } } - } - public var contnent: some View { - // let notificationDate = viewModel.date.addingTimeInterval(selection.timeInterval) - PageView("Notification") { - VStack(spacing: .zero) { - SectionView { - LazyVStack(spacing: .zero) { - ForEach(LocalNotificationTime.allCases) { notificationTime in + public var contnent: some View { + // let notificationDate = viewModel.date.addingTimeInterval(selection.timeInterval) + PageView("Notification") { + VStack(spacing: .zero) { + SectionView { + LazyVStack(spacing: .zero) { + ForEach(LocalNotificationTime.allCases) { notificationTime in // let notificationDate = viewModel.date.addingTimeInterval(notificationTime.timeInterval) // if notificationDate viewModel.date { - Radio(notificationTime.title, isOn: selection.id == notificationTime.id) { - selection = notificationTime + Radio(notificationTime.title, isOn: selection.id == notificationTime.id) { + selection = notificationTime + } + // } } - // } } } - } - .surfaceContentRowMargins() - if isPendingNotification { - SectionView { - VStack(spacing: .zero) { - Row("Delete notification") { - viewModel.deleteNotification() - saveAction?(nil) - isPendingNotification = false - dismiss() - } trailing: { - IconDeprecated(.trash) - .iconColor(Color.error) + .surfaceContentRowMargins() + if isPendingNotification { + SectionView { + VStack(spacing: .zero) { + Row("Delete notification") { + viewModel.deleteNotification() + saveAction?(nil) + isPendingNotification = false + dismiss() + } trailing: { + IconDeprecated(.trash) + .iconColor(Color.error) + } } } + .surfaceContentRowMargins() } - .surfaceContentRowMargins() } } - } - .backgroundSecondary() - .leadingBar { - BarButton(.close) - } - .trailingBar { - BarButton(.accent("Done", action: { - Task { - await viewModel.setNotification(timeBefore: selection) - saveAction?(viewModel.id) - isPendingNotification = true - dismiss() - } - })) + .backgroundSecondary() + .leadingBar { + BarButton(.close) + } + .trailingBar { + BarButton(.accent("Done", action: { + Task { + await viewModel.setNotification(timeBefore: selection) + saveAction?(viewModel.id) + isPendingNotification = true + dismiss() + } + })) + } } } -} +#endif diff --git a/Sources/OversizeNotificationKit/Model/LocalNotificationAlertsTimes.swift b/Sources/OversizeNotificationKit/Model/LocalNotificationAlertsTimes.swift index 2c45852..3ace115 100644 --- a/Sources/OversizeNotificationKit/Model/LocalNotificationAlertsTimes.swift +++ b/Sources/OversizeNotificationKit/Model/LocalNotificationAlertsTimes.swift @@ -3,10 +3,9 @@ // LocalNotificationAlertsTimes.swift // -import EventKit import Foundation -public enum LocalNotificationTime: CaseIterable, Equatable, Identifiable { +public enum LocalNotificationTime: CaseIterable, Equatable, Identifiable, @unchecked Sendable { case oneMinuteBefore, fiveMinutesBefore, tenMinutesBefore, thirtyMinutesBefore, oneHourBefore, twoHoursBefore, oneDayBefore, twoDaysBefore, oneWeekBefore public var title: String { @@ -59,5 +58,5 @@ public enum LocalNotificationTime: CaseIterable, Equatable, Identifiable { title } - public static var allCases: [LocalNotificationTime] = [.oneMinuteBefore, .fiveMinutesBefore, .tenMinutesBefore, .thirtyMinutesBefore, .oneHourBefore, .twoHoursBefore, .oneDayBefore, .twoDaysBefore, .oneWeekBefore] + public static let allCases: [LocalNotificationTime] = [.oneMinuteBefore, .fiveMinutesBefore, .tenMinutesBefore, .thirtyMinutesBefore, .oneHourBefore, .twoHoursBefore, .oneDayBefore, .twoDaysBefore, .oneWeekBefore] } diff --git a/Sources/OversizeOnboardingKit/OnboardingItem.swift b/Sources/OversizeOnboardingKit/OnboardingItem.swift deleted file mode 100644 index 76e1028..0000000 --- a/Sources/OversizeOnboardingKit/OnboardingItem.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright © 2022 Alexander Romanov -// OnboardingItem.swift -// - -import SwiftUI - -public struct OnboardingItem: Identifiable, Equatable { - public var id = UUID() - public var image: Image? - public var title: String? - public var subtitle: String? - - public init(id: UUID = UUID(), image: Image? = nil, title: String? = nil, subtitle: String? = nil) { - self.id = id - self.image = image - self.title = title - self.subtitle = subtitle - } -} diff --git a/Sources/OversizeOnboardingKit/OnboardingItemPreferenceKey.swift b/Sources/OversizeOnboardingKit/OnboardingItemPreferenceKey.swift deleted file mode 100644 index 0335c7f..0000000 --- a/Sources/OversizeOnboardingKit/OnboardingItemPreferenceKey.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Copyright © 2022 Alexander Romanov -// OnboardingItemPreferenceKey.swift -// - -import SwiftUI - -struct OnboardingItemPreferenceKey: PreferenceKey { - static var defaultValue: [OnboardingItem] = [] - - static func reduce(value: inout [OnboardingItem], nextValue: () -> [OnboardingItem]) { - value += nextValue() - } -} diff --git a/Sources/OversizeOnboardingKit/OnboardingItemViewModifier.swift b/Sources/OversizeOnboardingKit/OnboardingItemViewModifier.swift deleted file mode 100644 index 23590f6..0000000 --- a/Sources/OversizeOnboardingKit/OnboardingItemViewModifier.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright © 2022 Alexander Romanov -// OnboardingItemViewModifier.swift -// - -import SwiftUI - -struct OnboardingItemViewModifier: ViewModifier { - let onboardingItem: OnboardingItem - - func body(content: Content) -> some View { - content - .preference(key: OnboardingItemPreferenceKey.self, value: [onboardingItem]) - } -} - -public extension View { - func onboardingItem(_ label: () -> OnboardingItem) -> some View { - modifier(OnboardingItemViewModifier(onboardingItem: label())) - } -} diff --git a/Sources/OversizeOnboardingKit/OnboardingView.swift b/Sources/OversizeOnboardingKit/OnboardingView.swift index a12d76e..9b0235b 100644 --- a/Sources/OversizeOnboardingKit/OnboardingView.swift +++ b/Sources/OversizeOnboardingKit/OnboardingView.swift @@ -1,111 +1,131 @@ // -// Copyright © 2023 Alexander Romanov -// OnboardingView.swift +// Copyright © 2024 Alexander Romanov +// OnboardView.swift, created on 16.09.2024 // import OversizeUI import SwiftUI -public struct OnboardingView: View { - @Environment(\.screenSize) var screenSize - - @Binding private var selection: Int - - @Namespace private var onboardingItem - - @State private var slides: [OnboardingItem] = [] - - private let finishAction: (() -> Void)? - - private var content: Content - - public init(selection: Binding, finishAction: (() -> Void)? = nil, @ViewBuilder content: () -> Content) { +public struct OnboardView: View where A: View, C: View { + private let content: C + private let actions: Group + private let backAction: (() -> Void)? + private let skipAction: (() -> Void)? + private let helpAction: (() -> Void)? + + public init( + @ViewBuilder content: () -> C, + @ViewBuilder actions: () -> A, + backAction: (() -> Void)? = nil, + skipAction: (() -> Void)? = nil, + helpAction: (() -> Void)? = nil + ) { self.content = content() - self.finishAction = finishAction - _selection = selection + self.actions = Group { actions() } + self.backAction = backAction + self.skipAction = skipAction + self.helpAction = helpAction } public var body: some View { - ZStack { - VStack { - PageIndexView($selection, maxIndex: slides.count) - .padding(.top, .large) - Spacer() - } - - TabView(selection: $selection) { - // ForEach(Array(slides.enumerated()), id: \.offset) { index, element in + content + .ignoresSafeArea(.all) + .frame( + maxWidth: .infinity, + maxHeight: .infinity + ) + .safeAreaInset(edge: .top, content: topButtons) + .safeAreaInset(edge: .bottom, content: bottomButtons) + } - tabItem(tabItem: OnboardingItem(title: "Title", subtitle: "Sub"), index: 0) - // .tag(index) - // } - } + private func topButtons() -> some View { + HStack { #if os(iOS) - .tabViewStyle(.page(indexDisplayMode: .never)) - .indexViewStyle(.page(backgroundDisplayMode: .never)) + if helpAction != nil { + Button { + helpAction?() + } label: { + Text("Help") + } + .buttonStyle(.tertiary) + .controlBorderShape(.capsule) + .accent() + .controlSize(.mini) + } #endif + + Spacer() + + if skipAction != nil { + Button { + skipAction?() + } label: { + Text("Skip") + } + .buttonStyle(.tertiary) + .controlBorderShape(.capsule) + .accent() + #if !os(tvOS) + .controlSize(.mini) + #endif + } } - .background( - Color.backgroundSecondary.ignoresSafeArea() - ) - .onPreferenceChange(OnboardingItemPreferenceKey.self) { value in - slides = value - } + .padding(.medium) } - private func tabItem(tabItem: OnboardingItem, index _: Int) -> some View { - VStack(spacing: .small) { - if let image = tabItem.image { - image - } + private func bottomButtons() -> some View { + #if os(iOS) + HStack(spacing: .small) { + if let backAction { + Button { + backAction() + } label: { + Image.Base.arrowLeft.icon() + } + .buttonStyle(.quaternary) + .accentColor(.secondary) + } - VStack { - if let title = tabItem.title { - Text(title) - .largeTitle() - .foregroundColor(.onSurfaceHighEmphasis) - .padding(.bottom, .small) + VStack(spacing: .xxxSmall) { + actions + } + } + .padding(.medium) + #else + HStack(spacing: .xSmall) { + if let helpAction { + Button("Help", action: helpAction) + .help("Help") + #if !os(tvOS) + .controlSize(.extraLarge) + .buttonStyle(.bordered) + #endif } - if let subtitle = tabItem.subtitle { - Text(subtitle) - .foregroundColor(.onSurfaceMediumEmphasis) - .fontWeight(.regular) - .font(.system(size: 19)) + Spacer() + + if let backAction { + Button( + "Back", + action: backAction + ) + #if !os(tvOS) + .controlSize(.extraLarge) + #endif + .buttonStyle(.bordered) } + + actions + #if !os(tvOS) + .controlSize(.extraLarge) + #endif + .buttonStyle(.borderedProminent) } - .offset(y: screenSize.height < 812 ? -50 : 0) - } - .multilineTextAlignment(.center) - .frame(maxWidth: 320) - .offset(y: -50) - .padding(.bottom, .xLarge) + .padding(.small) + .background(Color.surfacePrimary) + .overlay(alignment: .top) { + Separator() + } + #endif } } - -// struct FloatingTabBarExample: View { -// @State var selection = 0 -// -// var body: some View { -// FloatingTabBar(selection: $selection, plusAction: { print("plus") }) { -// Color.gray -// .floatingTabItem { -// TabItem(icon: Image(systemName: "star")) -// } -// .opacity(selection == 0 ? 1 : 0) -// -// Color.blue -// .floatingTabItem { -// TabItem(icon: Image(systemName: "plus")) -// } -// .opacity(selection == 1 ? 1 : 0) -// } -// } -// } - -// struct FloatingTabBar_Previews: PreviewProvider { -// static var previews: some View { -// FloatingTabBarExample() -// } -// } -// diff --git a/Sources/OversizePhotoKit/PhotoOptionsView.swift b/Sources/OversizePhotoKit/PhotoOptionsView.swift index 8c4434f..904582c 100644 --- a/Sources/OversizePhotoKit/PhotoOptionsView.swift +++ b/Sources/OversizePhotoKit/PhotoOptionsView.swift @@ -25,7 +25,7 @@ public struct PhotoOptionsView: View where A: View { private let actions: Group? private let deleteAction: (() -> Void)? - @State private var isShowAlert: Bool = false + @State private var isShowAlert = false public init( image: Image, @@ -63,20 +63,21 @@ public struct PhotoOptionsView: View where A: View { VStack(spacing: .medium) { SectionView { VStack { - if #available(iOS 16.0, *) { - ShareLink( - item: photo, - preview: SharePreview( - "Photo", - image: photo.image - ) - ) { - Row("Share") { - Image.Base.upload + #if !os(tvOS) + if #available(iOS 16.0, *) { + ShareLink( + item: photo, + preview: SharePreview( + "Photo", + image: photo.image + ) + ) { + Row("Share") { + Image.Base.upload + } } } - } - + #endif actions } .buttonStyle(.row) diff --git a/Sources/OversizePhotoKit/PhotosGalleryView.swift b/Sources/OversizePhotoKit/PhotosGalleryView.swift index 7aff10e..5c13e1c 100644 --- a/Sources/OversizePhotoKit/PhotosGalleryView.swift +++ b/Sources/OversizePhotoKit/PhotosGalleryView.swift @@ -23,7 +23,7 @@ public struct PhotosGalleryView: View { if images.isEmpty { Text("Not photos") .title3() - .onSurfaceHighEmphasisForegroundColor() + .onSurfacePrimaryForeground() } else { ImageGridView(images, columnCount: .constant(3)) { image in let index = images.firstIndex(of: image)