From 63f633a767a3f1158ef9dfa3f60be9e31d8d9b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejn=C3=ADk?= Date: Wed, 7 Feb 2024 16:19:41 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=80=20Template=20merge=20(#143)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Add UI extensions from ProjectTemplate * ✨ Add Networking from template * ✨ Add VersionUpdateManager from template * ✨ Add FirebaseFetcher * ⬆️ Use Xcode 15.1 * 🔧 Build FirebaseFetcher on CI * 🔥 Get rid of FirebaseFetcher * ✨ Add PushNotifications target * 📝 Update changelog * 🔥 Remove deploy workflow * 🔧 Use Xcode 15.2 * 🔥 Cleanup --- .github/workflows/deploy.yml | 31 - .github/workflows/pr.yml | 2 +- .github/workflows/tests.yml | 12 +- .github/xcode-version | 2 +- .gitignore | 3 +- ACKategories.xcodeproj/project.pbxproj | 1017 ++++++++++++++++- ....xcscheme => ACKategoriesTesting.xcscheme} | 40 +- .../xcschemes/Networking.xcscheme | 85 ++ .../xcschemes/PushNotifications.xcscheme | 79 ++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + CHANGELOG.md | 4 + Package.swift | 24 +- .../ACKategories/{ => UI}/GradientView.swift | 0 .../UI/Popup/PopupModalAnimation.swift | 147 +++ .../UI/Popup/PopupPresenting.swift | 18 + Sources/ACKategories/{ => UI}/Reusable.swift | 0 .../ACKategories/{ => UI}/ReusableView.swift | 0 .../SelfSizingTableHeaderFooterView.swift | 0 .../ACKategories/{ => UI}/TagListView.swift | 0 .../UI/Theme/ThemeExtensions.swift | 9 + .../ACKategories/UI/Theme/ThemeProvider.swift | 11 + .../{ => UI}/UIApplicationExtensions.swift | 0 .../{ => UI}/UILabelExtensions.swift | 0 .../UINavigationControllerExtensions.swift | 0 .../{ => UI}/UISearchBarExtensions.swift | 0 .../{ => UI}/UIStackViewExtensions.swift | 0 .../ACKategories/{ => UI}/UIView+Spacer.swift | 0 .../{ => UI}/UIViewController+Children.swift | 0 .../{ => UI}/UIViewController+FrontMost.swift | 0 .../{ => UI}/UIViewExtensions.swift | 0 .../VersionUpdate/MinBuildNumberFetcher.swift | 6 + .../VersionUpdate/VersionUpdateManager.swift | 29 + .../Networking/APIServiceMock.swift | 41 + .../Networking/HTTPResponse+TestData.swift | 16 + .../Networking/HTTPURLResponse+TestData.swift | 17 + .../Networking/NetworkMock.swift | 15 + .../Networking/URL+TestData.swift | 6 + .../Networking/URLRequest+TestData.swift | 12 + .../VersionUpdateFetcher_Mock.swift | 10 + Sources/Networking/APIService.swift | 118 ++ Sources/Networking/APIServicing.swift | 52 + Sources/Networking/HTTPMethod.swift | 41 + Sources/Networking/HTTPResponse.swift | 28 + Sources/Networking/Networking.swift | 23 + Sources/Networking/OAuthInterceptor.swift | 68 ++ Sources/Networking/RequestAddress.swift | 22 + Sources/Networking/RequestBody.swift | 99 ++ Sources/PushNotifications/PushManager.swift | 142 +++ .../UNNotificationSettingsExtensions.swift | 22 + Tests/ACKategoriesTests/EdgeInsetsTests.swift | 1 + .../VersionUpdateManager_Tests.swift | 58 + .../APIService+OAuthInterceptor_Tests.swift | 176 +++ Tests/NetworkingTests/APIService_Tests.swift | 81 ++ Tests/NetworkingTests/Networking.xctestplan | 24 + .../OAuthInterceptor_Tests.swift | 247 ++++ .../RequestAddress_Tests.swift | 17 + 56 files changed, 2767 insertions(+), 96 deletions(-) delete mode 100644 .github/workflows/deploy.yml rename ACKategories.xcodeproj/xcshareddata/xcschemes/{ACKategoriesResponderTests.xcscheme => ACKategoriesTesting.xcscheme} (63%) create mode 100644 ACKategories.xcodeproj/xcshareddata/xcschemes/Networking.xcscheme create mode 100644 ACKategories.xcodeproj/xcshareddata/xcschemes/PushNotifications.xcscheme create mode 100644 ACKategories.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename Sources/ACKategories/{ => UI}/GradientView.swift (100%) create mode 100644 Sources/ACKategories/UI/Popup/PopupModalAnimation.swift create mode 100644 Sources/ACKategories/UI/Popup/PopupPresenting.swift rename Sources/ACKategories/{ => UI}/Reusable.swift (100%) rename Sources/ACKategories/{ => UI}/ReusableView.swift (100%) rename Sources/ACKategories/{ => UI}/SelfSizingTableHeaderFooterView.swift (100%) rename Sources/ACKategories/{ => UI}/TagListView.swift (100%) create mode 100644 Sources/ACKategories/UI/Theme/ThemeExtensions.swift create mode 100644 Sources/ACKategories/UI/Theme/ThemeProvider.swift rename Sources/ACKategories/{ => UI}/UIApplicationExtensions.swift (100%) rename Sources/ACKategories/{ => UI}/UILabelExtensions.swift (100%) rename Sources/ACKategories/{ => UI}/UINavigationControllerExtensions.swift (100%) rename Sources/ACKategories/{ => UI}/UISearchBarExtensions.swift (100%) rename Sources/ACKategories/{ => UI}/UIStackViewExtensions.swift (100%) rename Sources/ACKategories/{ => UI}/UIView+Spacer.swift (100%) rename Sources/ACKategories/{ => UI}/UIViewController+Children.swift (100%) rename Sources/ACKategories/{ => UI}/UIViewController+FrontMost.swift (100%) rename Sources/ACKategories/{ => UI}/UIViewExtensions.swift (100%) create mode 100644 Sources/ACKategories/VersionUpdate/MinBuildNumberFetcher.swift create mode 100644 Sources/ACKategories/VersionUpdate/VersionUpdateManager.swift create mode 100644 Sources/ACKategoriesTesting/Networking/APIServiceMock.swift create mode 100644 Sources/ACKategoriesTesting/Networking/HTTPResponse+TestData.swift create mode 100644 Sources/ACKategoriesTesting/Networking/HTTPURLResponse+TestData.swift create mode 100644 Sources/ACKategoriesTesting/Networking/NetworkMock.swift create mode 100644 Sources/ACKategoriesTesting/Networking/URL+TestData.swift create mode 100644 Sources/ACKategoriesTesting/Networking/URLRequest+TestData.swift create mode 100644 Sources/ACKategoriesTesting/VersionUpdateFetcher_Mock.swift create mode 100644 Sources/Networking/APIService.swift create mode 100644 Sources/Networking/APIServicing.swift create mode 100644 Sources/Networking/HTTPMethod.swift create mode 100644 Sources/Networking/HTTPResponse.swift create mode 100644 Sources/Networking/Networking.swift create mode 100644 Sources/Networking/OAuthInterceptor.swift create mode 100644 Sources/Networking/RequestAddress.swift create mode 100644 Sources/Networking/RequestBody.swift create mode 100644 Sources/PushNotifications/PushManager.swift create mode 100644 Sources/PushNotifications/UNNotificationSettingsExtensions.swift create mode 100644 Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift create mode 100644 Tests/NetworkingTests/APIService+OAuthInterceptor_Tests.swift create mode 100644 Tests/NetworkingTests/APIService_Tests.swift create mode 100644 Tests/NetworkingTests/Networking.xctestplan create mode 100644 Tests/NetworkingTests/OAuthInterceptor_Tests.swift create mode 100644 Tests/NetworkingTests/RequestAddress_Tests.swift diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 3121c1de..00000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Deploy - -on: - push: - tags: - - '*' - -jobs: - carthage: - name: Upload Carthage binary - runs-on: macos-13 - steps: - - uses: actions/checkout@v4 - - uses: AckeeCZ/load-xcode-version@1.1.0 - - name: Build - run: carthage build --no-skip-current --cache-builds --use-xcframeworks - - name: Archive - run: | - DST=$PWD - mkdir -p /tmp/ACKategories - mv Carthage/Build/*.xcframework /tmp/ACKategories - cd /tmp - zip -r "$DST/"ACKategories.xcframework.zip ACKategories - - uses: xresloader/upload-to-github-release@v1.3.12 - if: startsWith(github.ref, 'refs/tags/') - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - file: ACKategories.xcframework.zip - tags: true - draft: false diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 355144b7..6fba3542 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Changelog Reminder - uses: peterjgrainger/action-changelog-reminder@v1.2.0 + uses: peterjgrainger/action-changelog-reminder@v1.3.0 with: changelog_regex: 'CHANGELOG.md' env: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 55722fef..8f735cbf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,35 +13,35 @@ jobs: - uses: AckeeCZ/load-xcode-version@1.1.0 - name: iOS tests run: set -o pipefail && xcodebuild test -scheme ACKategories -resultBundlePath Tests-iOS.xcresult -sdk iphonesimulator -destination "platform=iOS Simulator,name=$IOS_DEVICE,OS=latest" ONLY_ACTIVE_ARCH=YES | xcpretty - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: Tests-iOS.xcresult path: Tests-iOS.xcresult - name: iOS responder tests run: set -o pipefail && xcodebuild test -scheme ACKategories -resultBundlePath Tests-iOS-Responder.xcresult -sdk iphonesimulator -destination "platform=iOS Simulator,name=$IOS_DEVICE,OS=latest" ONLY_ACTIVE_ARCH=YES | xcpretty - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: Tests-iOS-Responder.xcresult path: Tests-iOS-Responder.xcresult - name: macOS tests run: set -o pipefail && xcodebuild test -scheme ACKategories -resultBundlePath Tests-macOS.xcresult -destination 'platform=OS X,arch=x86_64' | xcpretty - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: Tests-macOS.xcresult path: Tests-macOS.xcresult - name: watchOS tests run: set -o pipefail && xcodebuild test -scheme ACKategories -resultBundlePath Tests-watchOS.xcresult -sdk watchsimulator -destination "platform=watchOS Simulator,name=Apple Watch Ultra 2 (49mm),OS=latest" ONLY_ACTIVE_ARCH=YES | xcpretty - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: Tests-watchOS.xcresult path: Tests-watchOS.xcresult - name: tvOS tests run: set -o pipefail && xcodebuild test -scheme ACKategories -resultBundlePath Tests-tvOS.xcresult -sdk appletvsimulator -destination "platform=tvOS Simulator,name=Apple TV 4K (3rd generation),OS=latest" ONLY_ACTIVE_ARCH=YES | xcpretty - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: Tests-tvOS.xcresult @@ -52,5 +52,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: AckeeCZ/load-xcode-version@1.1.0 + - name: SPM build + run: swift build - name: SPM tests run: swift test \ No newline at end of file diff --git a/.github/xcode-version b/.github/xcode-version index 9dc738e6..0d57595e 100644 --- a/.github/xcode-version +++ b/.github/xcode-version @@ -1 +1 @@ -15.0.1 \ No newline at end of file +15.2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3c2246c7..952c54d9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ Carthage /.build .DS_Store DerivedData -.swiftpm/ \ No newline at end of file +.swiftpm/ +xcuserdata diff --git a/ACKategories.xcodeproj/project.pbxproj b/ACKategories.xcodeproj/project.pbxproj index 3eff084e..07c6e7c5 100644 --- a/ACKategories.xcodeproj/project.pbxproj +++ b/ACKategories.xcodeproj/project.pbxproj @@ -7,14 +7,47 @@ objects = { /* Begin PBXBuildFile section */ + 690BCB8D2B3DB62400EDA6F8 /* Networking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 693A925D2B3DB290008B3DC3 /* Networking.framework */; }; 691B9AD92AFD1E5C008AE7BD /* ACKategories.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69ACD6C22AFD130C0021127B /* ACKategories.framework */; }; 691B9ADA2AFD1E5C008AE7BD /* ACKategories.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 69ACD6C22AFD130C0021127B /* ACKategories.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6922C77D2AFD1C1A00519CDF /* UINavigationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6922C77C2AFD1C1A00519CDF /* UINavigationControllerTests.swift */; }; 6922C7802AFD1C8B00519CDF /* Dummies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6A02AFD114600C8E8D9 /* Dummies.swift */; }; 6922C7812AFD1C8B00519CDF /* FlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6A12AFD114600C8E8D9 /* FlowCoordinatorTests.swift */; }; + 693A92652B3DB290008B3DC3 /* Networking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 693A925D2B3DB290008B3DC3 /* Networking.framework */; }; + 693A927B2B3DB2AA008B3DC3 /* RequestAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92732B3DB2AA008B3DC3 /* RequestAddress.swift */; }; + 693A927C2B3DB2AA008B3DC3 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92742B3DB2AA008B3DC3 /* APIService.swift */; }; + 693A927D2B3DB2AA008B3DC3 /* RequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92752B3DB2AA008B3DC3 /* RequestBody.swift */; }; + 693A927E2B3DB2AA008B3DC3 /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92762B3DB2AA008B3DC3 /* Networking.swift */; }; + 693A927F2B3DB2AA008B3DC3 /* HTTPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92772B3DB2AA008B3DC3 /* HTTPResponse.swift */; }; + 693A92802B3DB2AA008B3DC3 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92782B3DB2AA008B3DC3 /* HTTPMethod.swift */; }; + 693A92812B3DB2AA008B3DC3 /* APIServicing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92792B3DB2AA008B3DC3 /* APIServicing.swift */; }; + 693A92822B3DB2AA008B3DC3 /* OAuthInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A927A2B3DB2AA008B3DC3 /* OAuthInterceptor.swift */; }; + 693A92882B3DB353008B3DC3 /* APIService_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92842B3DB353008B3DC3 /* APIService_Tests.swift */; }; + 693A92892B3DB353008B3DC3 /* OAuthInterceptor_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92852B3DB353008B3DC3 /* OAuthInterceptor_Tests.swift */; }; + 693A928A2B3DB353008B3DC3 /* APIService+OAuthInterceptor_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92862B3DB353008B3DC3 /* APIService+OAuthInterceptor_Tests.swift */; }; + 693A928B2B3DB353008B3DC3 /* RequestAddress_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92872B3DB353008B3DC3 /* RequestAddress_Tests.swift */; }; + 693A92A02B3DB394008B3DC3 /* NetworkMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A929A2B3DB394008B3DC3 /* NetworkMock.swift */; }; + 693A92A12B3DB394008B3DC3 /* APIServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A929B2B3DB394008B3DC3 /* APIServiceMock.swift */; }; + 693A92A22B3DB394008B3DC3 /* URLRequest+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A929C2B3DB394008B3DC3 /* URLRequest+TestData.swift */; }; + 693A92A32B3DB394008B3DC3 /* HTTPURLResponse+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A929D2B3DB394008B3DC3 /* HTTPURLResponse+TestData.swift */; }; + 693A92A42B3DB394008B3DC3 /* HTTPResponse+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A929E2B3DB394008B3DC3 /* HTTPResponse+TestData.swift */; }; + 693A92A52B3DB394008B3DC3 /* URL+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A929F2B3DB394008B3DC3 /* URL+TestData.swift */; }; + 693A92A62B3DB39F008B3DC3 /* ACKategoriesTesting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 693A92912B3DB388008B3DC3 /* ACKategoriesTesting.framework */; }; + 694D14EB2B3DD61A0083E614 /* VersionUpdateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694D14E92B3DD6190083E614 /* VersionUpdateManager.swift */; }; + 694D14EC2B3DD61A0083E614 /* MinBuildNumberFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694D14EA2B3DD6190083E614 /* MinBuildNumberFetcher.swift */; }; + 694D14EE2B3DD64C0083E614 /* VersionUpdateFetcher_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694D14ED2B3DD64C0083E614 /* VersionUpdateFetcher_Mock.swift */; }; + 694D14F22B3DD66C0083E614 /* EdgeInsetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694D14EF2B3DD66C0083E614 /* EdgeInsetsTests.swift */; }; + 694D14F32B3DD66C0083E614 /* VersionUpdateManager_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694D14F12B3DD66C0083E614 /* VersionUpdateManager_Tests.swift */; }; + 694D14F42B3DD71C0083E614 /* ACKategories.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69ACD6C22AFD130C0021127B /* ACKategories.framework */; }; + 694D14F92B3DD7A40083E614 /* ACKategoriesTesting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 693A92912B3DB388008B3DC3 /* ACKategoriesTesting.framework */; }; 6971A98A2AFD1AF5000FC317 /* ACKategories.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69ACD6C22AFD130C0021127B /* ACKategories.framework */; }; 6971A9902AFD1B0F000FC317 /* ControlBlocksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6B62AFD114600C8E8D9 /* ControlBlocksTests.swift */; }; 697D61D42B0BC41900020664 /* EdgeInsetsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 697D61D32B0BC41900020664 /* EdgeInsetsExtensions.swift */; }; + 698376CF2B3DA81200CD9E89 /* PopupPresenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698376C92B3DA81200CD9E89 /* PopupPresenting.swift */; }; + 698376D02B3DA81200CD9E89 /* PopupModalAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698376CA2B3DA81200CD9E89 /* PopupModalAnimation.swift */; }; + 698376D12B3DA81200CD9E89 /* ThemeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698376CC2B3DA81200CD9E89 /* ThemeExtensions.swift */; }; + 698376D22B3DA81200CD9E89 /* ThemeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698376CD2B3DA81200CD9E89 /* ThemeProvider.swift */; }; + 698376D32B3DA81200CD9E89 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698376CE2B3DA81200CD9E89 /* GradientView.swift */; }; 69ACD6CA2AFD130D0021127B /* ACKategories.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69ACD6C22AFD130C0021127B /* ACKategories.framework */; }; 69ACD6D72AFD133A0021127B /* ACKategories.h in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A5F92AFD10BE00C8E8D9 /* ACKategories.h */; }; 69ACD6D82AFD133A0021127B /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A5F82AFD10BE00C8E8D9 /* ArrayExtensions.swift */; }; @@ -27,7 +60,6 @@ 69ACD6DF2AFD133A0021127B /* DateFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6082AFD10BE00C8E8D9 /* DateFormatting.swift */; }; 69ACD6E02AFD133A0021127B /* DictionaryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6052AFD10BE00C8E8D9 /* DictionaryExtensions.swift */; }; 69ACD6E12AFD133A0021127B /* ErrorHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6102AFD10BE00C8E8D9 /* ErrorHandlers.swift */; }; - 69ACD6E22AFD133A0021127B /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A61A2AFD10BE00C8E8D9 /* GradientView.swift */; }; 69ACD6E32AFD133A0021127B /* IntExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6172AFD10BE00C8E8D9 /* IntExtensions.swift */; }; 69ACD6E42AFD133A0021127B /* NSAttributedStringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6212AFD10BE00C8E8D9 /* NSAttributedStringExtensions.swift */; }; 69ACD6E52AFD133A0021127B /* NSMutableParagraphStyleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A60F2AFD10BE00C8E8D9 /* NSMutableParagraphStyleExtensions.swift */; }; @@ -83,6 +115,8 @@ 69ACD71C2AFD13480021127B /* IntRandomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6B22AFD114600C8E8D9 /* IntRandomTests.swift */; }; 69ACD71D2AFD13480021127B /* StringRandomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6B32AFD114600C8E8D9 /* StringRandomTests.swift */; }; 69C6BE902B0BC80E008A4ECF /* UIWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6A82AFD114600C8E8D9 /* UIWindow.swift */; }; + 69DF227B2B459D0D0025C555 /* PushManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DF22792B459D0D0025C555 /* PushManager.swift */; }; + 69DF227C2B459D0D0025C555 /* UNNotificationSettingsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DF227A2B459D0D0025C555 /* UNNotificationSettingsExtensions.swift */; }; 69FA5FAD23C868A900B44BCD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69FA5F9023C868A900B44BCD /* Assets.xcassets */; }; 69FA5FAE23C868A900B44BCD /* UIControlBlocksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FA5F9323C868A900B44BCD /* UIControlBlocksViewController.swift */; }; 69FA5FAF23C868A900B44BCD /* TitleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FA5F9523C868A900B44BCD /* TitleViewController.swift */; }; @@ -104,6 +138,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 690BCB8F2B3DB62400EDA6F8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 69E819DF23C773010054687B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 693A925C2B3DB290008B3DC3; + remoteInfo = Networking; + }; 691B9ADB2AFD1E5C008AE7BD /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 69E819DF23C773010054687B /* Project object */; @@ -111,6 +152,34 @@ remoteGlobalIDString = 69ACD6C12AFD130C0021127B; remoteInfo = ACKategories; }; + 693A92662B3DB290008B3DC3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 69E819DF23C773010054687B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 693A925C2B3DB290008B3DC3; + remoteInfo = Networking; + }; + 693A92A82B3DB39F008B3DC3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 69E819DF23C773010054687B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 693A92902B3DB388008B3DC3; + remoteInfo = ACKategoriesTesting; + }; + 694D14F62B3DD71C0083E614 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 69E819DF23C773010054687B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 69ACD6C12AFD130C0021127B; + remoteInfo = ACKategories; + }; + 694D14FB2B3DD7A40083E614 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 69E819DF23C773010054687B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 693A92902B3DB388008B3DC3; + remoteInfo = ACKategoriesTesting; + }; 6971A98B2AFD1AF5000FC317 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 69E819DF23C773010054687B /* Project object */; @@ -149,15 +218,50 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 690BCB8C2B3DB5DE00EDA6F8 /* Networking.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Networking.xctestplan; path = Tests/NetworkingTests/Networking.xctestplan; sourceTree = SOURCE_ROOT; }; 6922C77C2AFD1C1A00519CDF /* UINavigationControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UINavigationControllerTests.swift; sourceTree = ""; }; 6922C77E2AFD1C3300519CDF /* ACKategoriesResponder.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = ACKategoriesResponder.xctestplan; sourceTree = ""; }; 6922C77F2AFD1C5000519CDF /* ACKategories.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = ACKategories.xctestplan; sourceTree = ""; }; + 693A925D2B3DB290008B3DC3 /* Networking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Networking.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 693A92642B3DB290008B3DC3 /* NetworkingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NetworkingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 693A92732B3DB2AA008B3DC3 /* RequestAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestAddress.swift; sourceTree = ""; }; + 693A92742B3DB2AA008B3DC3 /* APIService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; + 693A92752B3DB2AA008B3DC3 /* RequestBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBody.swift; sourceTree = ""; }; + 693A92762B3DB2AA008B3DC3 /* Networking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = ""; }; + 693A92772B3DB2AA008B3DC3 /* HTTPResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPResponse.swift; sourceTree = ""; }; + 693A92782B3DB2AA008B3DC3 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + 693A92792B3DB2AA008B3DC3 /* APIServicing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIServicing.swift; sourceTree = ""; }; + 693A927A2B3DB2AA008B3DC3 /* OAuthInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuthInterceptor.swift; sourceTree = ""; }; + 693A92842B3DB353008B3DC3 /* APIService_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIService_Tests.swift; sourceTree = ""; }; + 693A92852B3DB353008B3DC3 /* OAuthInterceptor_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuthInterceptor_Tests.swift; sourceTree = ""; }; + 693A92862B3DB353008B3DC3 /* APIService+OAuthInterceptor_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+OAuthInterceptor_Tests.swift"; sourceTree = ""; }; + 693A92872B3DB353008B3DC3 /* RequestAddress_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestAddress_Tests.swift; sourceTree = ""; }; + 693A92912B3DB388008B3DC3 /* ACKategoriesTesting.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ACKategoriesTesting.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 693A929A2B3DB394008B3DC3 /* NetworkMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkMock.swift; sourceTree = ""; }; + 693A929B2B3DB394008B3DC3 /* APIServiceMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIServiceMock.swift; sourceTree = ""; }; + 693A929C2B3DB394008B3DC3 /* URLRequest+TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+TestData.swift"; sourceTree = ""; }; + 693A929D2B3DB394008B3DC3 /* HTTPURLResponse+TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPURLResponse+TestData.swift"; sourceTree = ""; }; + 693A929E2B3DB394008B3DC3 /* HTTPResponse+TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPResponse+TestData.swift"; sourceTree = ""; }; + 693A929F2B3DB394008B3DC3 /* URL+TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+TestData.swift"; sourceTree = ""; }; + 694D14E92B3DD6190083E614 /* VersionUpdateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionUpdateManager.swift; sourceTree = ""; }; + 694D14EA2B3DD6190083E614 /* MinBuildNumberFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinBuildNumberFetcher.swift; sourceTree = ""; }; + 694D14ED2B3DD64C0083E614 /* VersionUpdateFetcher_Mock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionUpdateFetcher_Mock.swift; sourceTree = ""; }; + 694D14EF2B3DD66C0083E614 /* EdgeInsetsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EdgeInsetsTests.swift; sourceTree = ""; }; + 694D14F12B3DD66C0083E614 /* VersionUpdateManager_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionUpdateManager_Tests.swift; sourceTree = ""; }; 695096D823C7908B00E8F457 /* ACKategoriesExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ACKategoriesExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6971A9862AFD1AF5000FC317 /* ACKategoriesResponderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ACKategoriesResponderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 697B023227DB65B50082F4AC /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 697D61D32B0BC41900020664 /* EdgeInsetsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EdgeInsetsExtensions.swift; sourceTree = ""; }; + 698376C92B3DA81200CD9E89 /* PopupPresenting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopupPresenting.swift; sourceTree = ""; }; + 698376CA2B3DA81200CD9E89 /* PopupModalAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopupModalAnimation.swift; sourceTree = ""; }; + 698376CC2B3DA81200CD9E89 /* ThemeExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeExtensions.swift; sourceTree = ""; }; + 698376CD2B3DA81200CD9E89 /* ThemeProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeProvider.swift; sourceTree = ""; }; + 698376CE2B3DA81200CD9E89 /* GradientView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; 69ACD6C22AFD130C0021127B /* ACKategories.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ACKategories.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 69ACD6C92AFD130D0021127B /* ACKategoriesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ACKategoriesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 69DF225A2B459CC50025C555 /* PushNotifications.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PushNotifications.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 69DF22792B459D0D0025C555 /* PushManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushManager.swift; sourceTree = ""; }; + 69DF227A2B459D0D0025C555 /* UNNotificationSettingsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UNNotificationSettingsExtensions.swift; sourceTree = ""; }; 69E0A5F52AFD10BE00C8E8D9 /* UISearchBarExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISearchBarExtensions.swift; sourceTree = ""; }; 69E0A5F72AFD10BE00C8E8D9 /* UserDefault.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = ""; }; 69E0A5F82AFD10BE00C8E8D9 /* ArrayExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayExtensions.swift; sourceTree = ""; }; @@ -192,7 +296,6 @@ 69E0A6172AFD10BE00C8E8D9 /* IntExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntExtensions.swift; sourceTree = ""; }; 69E0A6182AFD10BE00C8E8D9 /* DateExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = ""; }; 69E0A6192AFD10BE00C8E8D9 /* UILabelExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UILabelExtensions.swift; sourceTree = ""; }; - 69E0A61A2AFD10BE00C8E8D9 /* GradientView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; 69E0A61B2AFD10BE00C8E8D9 /* CollectionExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; sourceTree = ""; }; 69E0A61C2AFD10BE00C8E8D9 /* UIButtonExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIButtonExtensions.swift; sourceTree = ""; }; 69E0A61D2AFD10BE00C8E8D9 /* BetterURL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BetterURL.swift; sourceTree = ""; }; @@ -250,6 +353,31 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 693A925A2B3DB290008B3DC3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 693A92612B3DB290008B3DC3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 693A92652B3DB290008B3DC3 /* Networking.framework in Frameworks */, + 693A92A62B3DB39F008B3DC3 /* ACKategoriesTesting.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 693A928E2B3DB388008B3DC3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 694D14F42B3DD71C0083E614 /* ACKategories.framework in Frameworks */, + 690BCB8D2B3DB62400EDA6F8 /* Networking.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 695096D523C7908B00E8F457 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -278,17 +406,84 @@ buildActionMask = 2147483647; files = ( 69ACD6CA2AFD130D0021127B /* ACKategories.framework in Frameworks */, + 694D14F92B3DD7A40083E614 /* ACKategoriesTesting.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 69DF22572B459CC50025C555 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 691B9AD82AFD1E5C008AE7BD /* Frameworks */ = { + 693A92722B3DB2AA008B3DC3 /* Networking */ = { + isa = PBXGroup; + children = ( + 693A92732B3DB2AA008B3DC3 /* RequestAddress.swift */, + 693A92742B3DB2AA008B3DC3 /* APIService.swift */, + 693A92752B3DB2AA008B3DC3 /* RequestBody.swift */, + 693A92762B3DB2AA008B3DC3 /* Networking.swift */, + 693A92772B3DB2AA008B3DC3 /* HTTPResponse.swift */, + 693A92782B3DB2AA008B3DC3 /* HTTPMethod.swift */, + 693A92792B3DB2AA008B3DC3 /* APIServicing.swift */, + 693A927A2B3DB2AA008B3DC3 /* OAuthInterceptor.swift */, + ); + path = Networking; + sourceTree = ""; + }; + 693A92832B3DB353008B3DC3 /* NetworkingTests */ = { + isa = PBXGroup; + children = ( + 690BCB8C2B3DB5DE00EDA6F8 /* Networking.xctestplan */, + 693A92842B3DB353008B3DC3 /* APIService_Tests.swift */, + 693A92852B3DB353008B3DC3 /* OAuthInterceptor_Tests.swift */, + 693A92862B3DB353008B3DC3 /* APIService+OAuthInterceptor_Tests.swift */, + 693A92872B3DB353008B3DC3 /* RequestAddress_Tests.swift */, + ); + path = NetworkingTests; + sourceTree = ""; + }; + 693A92982B3DB394008B3DC3 /* ACKategoriesTesting */ = { + isa = PBXGroup; + children = ( + 694D14ED2B3DD64C0083E614 /* VersionUpdateFetcher_Mock.swift */, + 693A92992B3DB394008B3DC3 /* Networking */, + ); + path = ACKategoriesTesting; + sourceTree = ""; + }; + 693A92992B3DB394008B3DC3 /* Networking */ = { + isa = PBXGroup; + children = ( + 693A929A2B3DB394008B3DC3 /* NetworkMock.swift */, + 693A929B2B3DB394008B3DC3 /* APIServiceMock.swift */, + 693A929C2B3DB394008B3DC3 /* URLRequest+TestData.swift */, + 693A929D2B3DB394008B3DC3 /* HTTPURLResponse+TestData.swift */, + 693A929E2B3DB394008B3DC3 /* HTTPResponse+TestData.swift */, + 693A929F2B3DB394008B3DC3 /* URL+TestData.swift */, + ); + path = Networking; + sourceTree = ""; + }; + 694D14E82B3DD6190083E614 /* VersionUpdate */ = { + isa = PBXGroup; + children = ( + 694D14E92B3DD6190083E614 /* VersionUpdateManager.swift */, + 694D14EA2B3DD6190083E614 /* MinBuildNumberFetcher.swift */, + ); + path = VersionUpdate; + sourceTree = ""; + }; + 694D14F02B3DD66C0083E614 /* VersionUpdate */ = { isa = PBXGroup; children = ( + 694D14F12B3DD66C0083E614 /* VersionUpdateManager_Tests.swift */, ); - name = Frameworks; + path = VersionUpdate; sourceTree = ""; }; 695096D923C7908B00E8F457 /* ACKategoriesExample */ = { @@ -318,10 +513,63 @@ path = ACKategoriesResponderTests; sourceTree = ""; }; + 698376C72B3DA81200CD9E89 /* UI */ = { + isa = PBXGroup; + children = ( + 698376CE2B3DA81200CD9E89 /* GradientView.swift */, + 69E0A6062AFD10BE00C8E8D9 /* Reusable.swift */, + 69E0A61E2AFD10BE00C8E8D9 /* ReusableView.swift */, + 69E0A5FA2AFD10BE00C8E8D9 /* SelfSizingTableHeaderFooterView.swift */, + 69E0A6112AFD10BE00C8E8D9 /* TagListView.swift */, + 69E0A61F2AFD10BE00C8E8D9 /* UIApplicationExtensions.swift */, + 69E0A6192AFD10BE00C8E8D9 /* UILabelExtensions.swift */, + 69E0A6132AFD10BE00C8E8D9 /* UINavigationControllerExtensions.swift */, + 69E0A5F52AFD10BE00C8E8D9 /* UISearchBarExtensions.swift */, + 69E0A5FB2AFD10BE00C8E8D9 /* UIStackViewExtensions.swift */, + 69E0A6092AFD10BE00C8E8D9 /* UIView+Spacer.swift */, + 69E0A60E2AFD10BE00C8E8D9 /* UIViewController+Children.swift */, + 69E0A5FC2AFD10BE00C8E8D9 /* UIViewController+FrontMost.swift */, + 69E0A6142AFD10BE00C8E8D9 /* UIViewExtensions.swift */, + 698376C82B3DA81200CD9E89 /* Popup */, + 698376CB2B3DA81200CD9E89 /* Theme */, + ); + path = UI; + sourceTree = ""; + }; + 698376C82B3DA81200CD9E89 /* Popup */ = { + isa = PBXGroup; + children = ( + 698376C92B3DA81200CD9E89 /* PopupPresenting.swift */, + 698376CA2B3DA81200CD9E89 /* PopupModalAnimation.swift */, + ); + path = Popup; + sourceTree = ""; + }; + 698376CB2B3DA81200CD9E89 /* Theme */ = { + isa = PBXGroup; + children = ( + 698376CC2B3DA81200CD9E89 /* ThemeExtensions.swift */, + 698376CD2B3DA81200CD9E89 /* ThemeProvider.swift */, + ); + path = Theme; + sourceTree = ""; + }; + 69DF22782B459D0D0025C555 /* PushNotifications */ = { + isa = PBXGroup; + children = ( + 69DF22792B459D0D0025C555 /* PushManager.swift */, + 69DF227A2B459D0D0025C555 /* UNNotificationSettingsExtensions.swift */, + ); + path = PushNotifications; + sourceTree = ""; + }; 69E0A5F32AFD10BE00C8E8D9 /* Sources */ = { isa = PBXGroup; children = ( 69E0A5F42AFD10BE00C8E8D9 /* ACKategories */, + 693A92982B3DB394008B3DC3 /* ACKategoriesTesting */, + 693A92722B3DB2AA008B3DC3 /* Networking */, + 69DF22782B459D0D0025C555 /* PushNotifications */, ); path = Sources; sourceTree = ""; @@ -331,6 +579,7 @@ children = ( 69E0A5F92AFD10BE00C8E8D9 /* ACKategories.h */, 69E0A5F82AFD10BE00C8E8D9 /* ArrayExtensions.swift */, + 6A72B2212B1A15AC00A59EDD /* BackGesture.swift */, 69E0A61D2AFD10BE00C8E8D9 /* BetterURL.swift */, 69E0A6152AFD10BE00C8E8D9 /* BundleExtensions.swift */, 69E0A61B2AFD10BE00C8E8D9 /* CollectionExtensions.swift */, @@ -341,38 +590,25 @@ 69E0A6052AFD10BE00C8E8D9 /* DictionaryExtensions.swift */, 697D61D32B0BC41900020664 /* EdgeInsetsExtensions.swift */, 69E0A6102AFD10BE00C8E8D9 /* ErrorHandlers.swift */, - 69E0A61A2AFD10BE00C8E8D9 /* GradientView.swift */, 69E0A6172AFD10BE00C8E8D9 /* IntExtensions.swift */, 69E0A6212AFD10BE00C8E8D9 /* NSAttributedStringExtensions.swift */, 69E0A60F2AFD10BE00C8E8D9 /* NSMutableParagraphStyleExtensions.swift */, 69E0A60D2AFD10BE00C8E8D9 /* NumberFormatterExtensions.swift */, 69E0A6282AFD10BE00C8E8D9 /* PublisherExtensions.swift */, - 69E0A6062AFD10BE00C8E8D9 /* Reusable.swift */, - 69E0A61E2AFD10BE00C8E8D9 /* ReusableView.swift */, - 69E0A5FA2AFD10BE00C8E8D9 /* SelfSizingTableHeaderFooterView.swift */, 69E0A6072AFD10BE00C8E8D9 /* StringExtensions.swift */, - 69E0A6112AFD10BE00C8E8D9 /* TagListView.swift */, - 69E0A61F2AFD10BE00C8E8D9 /* UIApplicationExtensions.swift */, 69E0A61C2AFD10BE00C8E8D9 /* UIButtonExtensions.swift */, 69E0A6292AFD10BE00C8E8D9 /* UIColorExtensions.swift */, 69E0A60A2AFD10BE00C8E8D9 /* UIControl+Blocks.swift */, 69E0A60C2AFD10BE00C8E8D9 /* UIControlEvents.swift */, 69E0A6202AFD10BE00C8E8D9 /* UIDeviceExtensions.swift */, 69E0A6122AFD10BE00C8E8D9 /* UIImageExtensions.swift */, - 69E0A6192AFD10BE00C8E8D9 /* UILabelExtensions.swift */, - 69E0A6132AFD10BE00C8E8D9 /* UINavigationControllerExtensions.swift */, - 69E0A5F52AFD10BE00C8E8D9 /* UISearchBarExtensions.swift */, - 69E0A5FB2AFD10BE00C8E8D9 /* UIStackViewExtensions.swift */, - 69E0A6092AFD10BE00C8E8D9 /* UIView+Spacer.swift */, - 69E0A60E2AFD10BE00C8E8D9 /* UIViewController+Children.swift */, - 69E0A5FC2AFD10BE00C8E8D9 /* UIViewController+FrontMost.swift */, - 69E0A6142AFD10BE00C8E8D9 /* UIViewExtensions.swift */, 69E0A5FD2AFD10BE00C8E8D9 /* UserDefaultsExtensions.swift */, 69E0A6222AFD10BE00C8E8D9 /* Base */, 69E0A5F62AFD10BE00C8E8D9 /* PropertyWrappers */, 69E0A5FE2AFD10BE00C8E8D9 /* RandomExtensions */, 69E0A6032AFD10BE00C8E8D9 /* SwiftUIExtensions */, - 6A72B2212B1A15AC00A59EDD /* BackGesture.swift */, + 698376C72B3DA81200CD9E89 /* UI */, + 694D14E82B3DD6190083E614 /* VersionUpdate */, ); path = ACKategories; sourceTree = ""; @@ -421,6 +657,7 @@ children = ( 6971A9872AFD1AF5000FC317 /* ACKategoriesResponderTests */, 69E0A69C2AFD114600C8E8D9 /* ACKategoriesTests */, + 693A92832B3DB353008B3DC3 /* NetworkingTests */, ); path = Tests; sourceTree = ""; @@ -434,6 +671,7 @@ 69E0A6AC2AFD114600C8E8D9 /* ColorTests.swift */, 69E0A6AF2AFD114600C8E8D9 /* ConditionalAssignmentTests.swift */, 69E0A6B42AFD114600C8E8D9 /* DateFormattingTests.swift */, + 694D14EF2B3DD66C0083E614 /* EdgeInsetsTests.swift */, 69E0A6AE2AFD114600C8E8D9 /* FoundationTests.swift */, 69E0A6A22AFD114600C8E8D9 /* IntTests.swift */, 69E0A6AB2AFD114600C8E8D9 /* ReusableViewTests.swift */, @@ -444,6 +682,7 @@ 6922C77F2AFD1C5000519CDF /* ACKategories.xctestplan */, 69E0A69D2AFD114600C8E8D9 /* PropertyWrappers */, 69E0A6B02AFD114600C8E8D9 /* Random */, + 694D14F02B3DD66C0083E614 /* VersionUpdate */, ); path = ACKategoriesTests; sourceTree = ""; @@ -491,7 +730,6 @@ 69E819EB23C773240054687B /* Products */, 69E0A5F32AFD10BE00C8E8D9 /* Sources */, 69E0A69B2AFD114600C8E8D9 /* Tests */, - 691B9AD82AFD1E5C008AE7BD /* Frameworks */, ); sourceTree = ""; }; @@ -502,6 +740,10 @@ 69ACD6C22AFD130C0021127B /* ACKategories.framework */, 69ACD6C92AFD130D0021127B /* ACKategoriesTests.xctest */, 6971A9862AFD1AF5000FC317 /* ACKategoriesResponderTests.xctest */, + 693A925D2B3DB290008B3DC3 /* Networking.framework */, + 693A92642B3DB290008B3DC3 /* NetworkingTests.xctest */, + 693A92912B3DB388008B3DC3 /* ACKategoriesTesting.framework */, + 69DF225A2B459CC50025C555 /* PushNotifications.framework */, ); name = Products; sourceTree = ""; @@ -607,6 +849,20 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + 693A92582B3DB290008B3DC3 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 693A928C2B3DB388008B3DC3 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 69ACD6BD2AFD130C0021127B /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -614,9 +870,73 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 69DF22552B459CC50025C555 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 693A925C2B3DB290008B3DC3 /* Networking */ = { + isa = PBXNativeTarget; + buildConfigurationList = 693A92702B3DB290008B3DC3 /* Build configuration list for PBXNativeTarget "Networking" */; + buildPhases = ( + 693A92582B3DB290008B3DC3 /* Headers */, + 693A92592B3DB290008B3DC3 /* Sources */, + 693A925A2B3DB290008B3DC3 /* Frameworks */, + 693A925B2B3DB290008B3DC3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Networking; + productName = Networking; + productReference = 693A925D2B3DB290008B3DC3 /* Networking.framework */; + productType = "com.apple.product-type.framework"; + }; + 693A92632B3DB290008B3DC3 /* NetworkingTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 693A92712B3DB290008B3DC3 /* Build configuration list for PBXNativeTarget "NetworkingTests" */; + buildPhases = ( + 693A92602B3DB290008B3DC3 /* Sources */, + 693A92612B3DB290008B3DC3 /* Frameworks */, + 693A92622B3DB290008B3DC3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 693A92672B3DB290008B3DC3 /* PBXTargetDependency */, + 693A92A92B3DB39F008B3DC3 /* PBXTargetDependency */, + ); + name = NetworkingTests; + productName = NetworkingTests; + productReference = 693A92642B3DB290008B3DC3 /* NetworkingTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 693A92902B3DB388008B3DC3 /* ACKategoriesTesting */ = { + isa = PBXNativeTarget; + buildConfigurationList = 693A92952B3DB388008B3DC3 /* Build configuration list for PBXNativeTarget "ACKategoriesTesting" */; + buildPhases = ( + 693A928C2B3DB388008B3DC3 /* Headers */, + 693A928D2B3DB388008B3DC3 /* Sources */, + 693A928E2B3DB388008B3DC3 /* Frameworks */, + 693A928F2B3DB388008B3DC3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 690BCB902B3DB62400EDA6F8 /* PBXTargetDependency */, + 694D14F72B3DD71C0083E614 /* PBXTargetDependency */, + ); + name = ACKategoriesTesting; + productName = ACKategoriesTesting; + productReference = 693A92912B3DB388008B3DC3 /* ACKategoriesTesting.framework */; + productType = "com.apple.product-type.framework"; + }; 695096D723C7908B00E8F457 /* ACKategoriesExample */ = { isa = PBXNativeTarget; buildConfigurationList = 695096E923C7908D00E8F457 /* Build configuration list for PBXNativeTarget "ACKategoriesExample" */; @@ -687,12 +1007,31 @@ ); dependencies = ( 69ACD6CC2AFD130D0021127B /* PBXTargetDependency */, + 694D14FC2B3DD7A40083E614 /* PBXTargetDependency */, ); name = ACKategoriesTests; productName = ACKategoriesTests; productReference = 69ACD6C92AFD130D0021127B /* ACKategoriesTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 69DF22592B459CC50025C555 /* PushNotifications */ = { + isa = PBXNativeTarget; + buildConfigurationList = 69DF226D2B459CC50025C555 /* Build configuration list for PBXNativeTarget "PushNotifications" */; + buildPhases = ( + 69DF22552B459CC50025C555 /* Headers */, + 69DF22562B459CC50025C555 /* Sources */, + 69DF22572B459CC50025C555 /* Frameworks */, + 69DF22582B459CC50025C555 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PushNotifications; + productName = PushNotifications; + productReference = 69DF225A2B459CC50025C555 /* PushNotifications.framework */; + productType = "com.apple.product-type.framework"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -700,9 +1039,18 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1500; + LastSwiftUpdateCheck = 1510; LastUpgradeCheck = 1500; TargetAttributes = { + 693A925C2B3DB290008B3DC3 = { + CreatedOnToolsVersion = 15.1; + }; + 693A92632B3DB290008B3DC3 = { + CreatedOnToolsVersion = 15.1; + }; + 693A92902B3DB388008B3DC3 = { + CreatedOnToolsVersion = 15.1; + }; 695096D723C7908B00E8F457 = { CreatedOnToolsVersion = 11.3; LastSwiftMigration = 1130; @@ -717,6 +1065,9 @@ 69ACD6C82AFD130D0021127B = { CreatedOnToolsVersion = 15.0.1; }; + 69DF22592B459CC50025C555 = { + CreatedOnToolsVersion = 15.1; + }; }; }; buildConfigurationList = 69E819E223C773010054687B /* Build configuration list for PBXProject "ACKategories" */; @@ -738,11 +1089,36 @@ 69ACD6C12AFD130C0021127B /* ACKategories */, 69ACD6C82AFD130D0021127B /* ACKategoriesTests */, 6971A9852AFD1AF5000FC317 /* ACKategoriesResponderTests */, + 693A925C2B3DB290008B3DC3 /* Networking */, + 693A92632B3DB290008B3DC3 /* NetworkingTests */, + 693A92902B3DB388008B3DC3 /* ACKategoriesTesting */, + 69DF22592B459CC50025C555 /* PushNotifications */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 693A925B2B3DB290008B3DC3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 693A92622B3DB290008B3DC3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 693A928F2B3DB388008B3DC3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 695096D623C7908B00E8F457 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -773,9 +1149,56 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 69DF22582B459CC50025C555 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 693A92592B3DB290008B3DC3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 693A927C2B3DB2AA008B3DC3 /* APIService.swift in Sources */, + 693A92822B3DB2AA008B3DC3 /* OAuthInterceptor.swift in Sources */, + 693A927B2B3DB2AA008B3DC3 /* RequestAddress.swift in Sources */, + 693A92802B3DB2AA008B3DC3 /* HTTPMethod.swift in Sources */, + 693A927F2B3DB2AA008B3DC3 /* HTTPResponse.swift in Sources */, + 693A927D2B3DB2AA008B3DC3 /* RequestBody.swift in Sources */, + 693A92812B3DB2AA008B3DC3 /* APIServicing.swift in Sources */, + 693A927E2B3DB2AA008B3DC3 /* Networking.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 693A92602B3DB290008B3DC3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 693A92882B3DB353008B3DC3 /* APIService_Tests.swift in Sources */, + 693A928A2B3DB353008B3DC3 /* APIService+OAuthInterceptor_Tests.swift in Sources */, + 693A928B2B3DB353008B3DC3 /* RequestAddress_Tests.swift in Sources */, + 693A92892B3DB353008B3DC3 /* OAuthInterceptor_Tests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 693A928D2B3DB388008B3DC3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 693A92A32B3DB394008B3DC3 /* HTTPURLResponse+TestData.swift in Sources */, + 693A92A52B3DB394008B3DC3 /* URL+TestData.swift in Sources */, + 693A92A12B3DB394008B3DC3 /* APIServiceMock.swift in Sources */, + 693A92A42B3DB394008B3DC3 /* HTTPResponse+TestData.swift in Sources */, + 693A92A02B3DB394008B3DC3 /* NetworkMock.swift in Sources */, + 693A92A22B3DB394008B3DC3 /* URLRequest+TestData.swift in Sources */, + 694D14EE2B3DD64C0083E614 /* VersionUpdateFetcher_Mock.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 695096D423C7908B00E8F457 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -815,21 +1238,25 @@ buildActionMask = 2147483647; files = ( 69ACD6D72AFD133A0021127B /* ACKategories.h in Sources */, + 698376D02B3DA81200CD9E89 /* PopupModalAnimation.swift in Sources */, + 698376D32B3DA81200CD9E89 /* GradientView.swift in Sources */, 69ACD6D82AFD133A0021127B /* ArrayExtensions.swift in Sources */, 69ACD6D92AFD133A0021127B /* BetterURL.swift in Sources */, 69ACD6DA2AFD133A0021127B /* BundleExtensions.swift in Sources */, 69ACD6DB2AFD133A0021127B /* CollectionExtensions.swift in Sources */, 69ACD6DC2AFD133A0021127B /* Combine+Concurrency.swift in Sources */, 69ACD6DD2AFD133A0021127B /* ConditionalAssignment.swift in Sources */, + 694D14EB2B3DD61A0083E614 /* VersionUpdateManager.swift in Sources */, 69ACD6DE2AFD133A0021127B /* DateExtensions.swift in Sources */, 69ACD6DF2AFD133A0021127B /* DateFormatting.swift in Sources */, 69ACD6E02AFD133A0021127B /* DictionaryExtensions.swift in Sources */, 69ACD6E12AFD133A0021127B /* ErrorHandlers.swift in Sources */, - 69ACD6E22AFD133A0021127B /* GradientView.swift in Sources */, 69ACD6E32AFD133A0021127B /* IntExtensions.swift in Sources */, 69ACD6E42AFD133A0021127B /* NSAttributedStringExtensions.swift in Sources */, + 694D14EC2B3DD61A0083E614 /* MinBuildNumberFetcher.swift in Sources */, 69ACD6E52AFD133A0021127B /* NSMutableParagraphStyleExtensions.swift in Sources */, 69ACD6E62AFD133A0021127B /* NumberFormatterExtensions.swift in Sources */, + 698376D22B3DA81200CD9E89 /* ThemeProvider.swift in Sources */, 697D61D42B0BC41900020664 /* EdgeInsetsExtensions.swift in Sources */, 69ACD6E72AFD133A0021127B /* PublisherExtensions.swift in Sources */, 69ACD6E82AFD133A0021127B /* Reusable.swift in Sources */, @@ -855,7 +1282,9 @@ 69ACD6FB2AFD133A0021127B /* UIViewExtensions.swift in Sources */, 69ACD6FC2AFD133A0021127B /* UserDefaultsExtensions.swift in Sources */, 69ACD6FD2AFD133A0021127B /* ViewController.swift in Sources */, + 698376D12B3DA81200CD9E89 /* ThemeExtensions.swift in Sources */, 69ACD6FE2AFD133A0021127B /* FlowCoordinator.swift in Sources */, + 698376CF2B3DA81200CD9E89 /* PopupPresenting.swift in Sources */, 69ACD6FF2AFD133A0021127B /* Logger.swift in Sources */, 69ACD7002AFD133A0021127B /* ViewModel.swift in Sources */, 69ACD7012AFD133A0021127B /* Base.swift in Sources */, @@ -876,7 +1305,9 @@ 69ACD7092AFD13480021127B /* BetterURL_Tests.swift in Sources */, 69ACD70A2AFD13480021127B /* CollectionTests.swift in Sources */, 69ACD70B2AFD13480021127B /* ColorTests.swift in Sources */, + 694D14F22B3DD66C0083E614 /* EdgeInsetsTests.swift in Sources */, 69ACD70C2AFD13480021127B /* ConditionalAssignmentTests.swift in Sources */, + 694D14F32B3DD66C0083E614 /* VersionUpdateManager_Tests.swift in Sources */, 69ACD70E2AFD13480021127B /* DateFormattingTests.swift in Sources */, 69ACD70F2AFD13480021127B /* FoundationTests.swift in Sources */, 69ACD7102AFD13480021127B /* IntTests.swift in Sources */, @@ -892,14 +1323,48 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 69DF22562B459CC50025C555 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 69DF227C2B459D0D0025C555 /* UNNotificationSettingsExtensions.swift in Sources */, + 69DF227B2B459D0D0025C555 /* PushManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 690BCB902B3DB62400EDA6F8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 693A925C2B3DB290008B3DC3 /* Networking */; + targetProxy = 690BCB8F2B3DB62400EDA6F8 /* PBXContainerItemProxy */; + }; 691B9ADC2AFD1E5C008AE7BD /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 69ACD6C12AFD130C0021127B /* ACKategories */; targetProxy = 691B9ADB2AFD1E5C008AE7BD /* PBXContainerItemProxy */; }; + 693A92672B3DB290008B3DC3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 693A925C2B3DB290008B3DC3 /* Networking */; + targetProxy = 693A92662B3DB290008B3DC3 /* PBXContainerItemProxy */; + }; + 693A92A92B3DB39F008B3DC3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 693A92902B3DB388008B3DC3 /* ACKategoriesTesting */; + targetProxy = 693A92A82B3DB39F008B3DC3 /* PBXContainerItemProxy */; + }; + 694D14F72B3DD71C0083E614 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 69ACD6C12AFD130C0021127B /* ACKategories */; + targetProxy = 694D14F62B3DD71C0083E614 /* PBXContainerItemProxy */; + }; + 694D14FC2B3DD7A40083E614 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 693A92902B3DB388008B3DC3 /* ACKategoriesTesting */; + targetProxy = 694D14FB2B3DD7A40083E614 /* PBXContainerItemProxy */; + }; 6971A98C2AFD1AF5000FC317 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 69ACD6C12AFD130C0021127B /* ACKategories */; @@ -918,35 +1383,350 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 695096EA23C7908D00E8F457 /* Debug */ = { + 693A926C2B3DB290008B3DC3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = cz.ackee.Networking; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 693A926D2B3DB290008B3DC3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = cz.ackee.Networking; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 693A926E2B3DB290008B3DC3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = cz.ackee.NetworkingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 693A926F2B3DB290008B3DC3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = cz.ackee.NetworkingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 693A92962B3DB388008B3DC3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = cz.ackee.ACKategoriesTesting; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 693A92972B3DB388008B3DC3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = cz.ackee.ACKategoriesTesting; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 695096EA23C7908D00E8F457 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -1198,12 +1978,14 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -1256,10 +2038,12 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -1268,7 +2052,6 @@ 69ACD6D52AFD130D0021127B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; @@ -1315,7 +2098,6 @@ 69ACD6D62AFD130D0021127B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; @@ -1352,6 +2134,121 @@ }; name = Release; }; + 69DF22692B459CC50025C555 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = cz.ackee.PushNotifications; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 69DF226A2B459CC50025C555 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = cz.ackee.PushNotifications; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; 69E819E323C773010054687B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1442,6 +2339,33 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 693A92702B3DB290008B3DC3 /* Build configuration list for PBXNativeTarget "Networking" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 693A926C2B3DB290008B3DC3 /* Debug */, + 693A926D2B3DB290008B3DC3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 693A92712B3DB290008B3DC3 /* Build configuration list for PBXNativeTarget "NetworkingTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 693A926E2B3DB290008B3DC3 /* Debug */, + 693A926F2B3DB290008B3DC3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 693A92952B3DB388008B3DC3 /* Build configuration list for PBXNativeTarget "ACKategoriesTesting" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 693A92962B3DB388008B3DC3 /* Debug */, + 693A92972B3DB388008B3DC3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 695096E923C7908D00E8F457 /* Build configuration list for PBXNativeTarget "ACKategoriesExample" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1478,6 +2402,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 69DF226D2B459CC50025C555 /* Build configuration list for PBXNativeTarget "PushNotifications" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 69DF22692B459CC50025C555 /* Debug */, + 69DF226A2B459CC50025C555 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 69E819E223C773010054687B /* Build configuration list for PBXProject "ACKategories" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ACKategories.xcodeproj/xcshareddata/xcschemes/ACKategoriesResponderTests.xcscheme b/ACKategories.xcodeproj/xcshareddata/xcschemes/ACKategoriesTesting.xcscheme similarity index 63% rename from ACKategories.xcodeproj/xcshareddata/xcschemes/ACKategoriesResponderTests.xcscheme rename to ACKategories.xcodeproj/xcshareddata/xcschemes/ACKategoriesTesting.xcscheme index c9cf2943..e83e29d9 100644 --- a/ACKategories.xcodeproj/xcshareddata/xcschemes/ACKategoriesResponderTests.xcscheme +++ b/ACKategories.xcodeproj/xcshareddata/xcschemes/ACKategoriesTesting.xcscheme @@ -1,10 +1,26 @@ + + + + + + - - - - - - + + + + diff --git a/ACKategories.xcodeproj/xcshareddata/xcschemes/Networking.xcscheme b/ACKategories.xcodeproj/xcshareddata/xcschemes/Networking.xcscheme new file mode 100644 index 00000000..f2bbae9e --- /dev/null +++ b/ACKategories.xcodeproj/xcshareddata/xcschemes/Networking.xcscheme @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ACKategories.xcodeproj/xcshareddata/xcschemes/PushNotifications.xcscheme b/ACKategories.xcodeproj/xcshareddata/xcschemes/PushNotifications.xcscheme new file mode 100644 index 00000000..124585d1 --- /dev/null +++ b/ACKategories.xcodeproj/xcshareddata/xcschemes/PushNotifications.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ACKategories.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ACKategories.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/ACKategories.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf72a4d..27775b10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ``` ## Next +- Merge [iOS template lib](https://github.com/AckeeCZ/iOS-MVVM-ProjectTemplate) ([#143](https://github.com/AckeeCZ/ACKategories/pull/143), kudos to @olejnjak) + - add Networking module + - add VersionUpdate module + - add PushNotifications module - Add `isTV` and `isMac` for `UIDevice` ([#142](https://github.com/AckeeCZ/ACKategories/pull/142), kudos to @leinhauplk) - Add helper function for easier back gesture setup ([#141](https://github.com/AckeeCZ/ACKategories/pull/141), kudos to @leinhauplk) - Support tvOS & watchOS, use single multiplatform target for Carthage ([#140](https://github.com/AckeeCZ/ACKategories/pull/140), kudos to @olejnjak) diff --git a/Package.swift b/Package.swift index bd15de2e..d03ecffc 100644 --- a/Package.swift +++ b/Package.swift @@ -11,12 +11,34 @@ let package = Package( ], products: [ .library(name: "ACKategories", targets: ["ACKategories"]), + .library(name: "ACKategoriesTesting", targets: ["ACKategoriesTesting"]), + .library(name: "Networking", targets: ["Networking"]), + .library(name: "PushNotifications", targets: ["PushNotifications"]), ], targets: [ .target(name: "ACKategories"), .testTarget( name: "ACKategoriesTests", - dependencies: ["ACKategories"] + dependencies: [ + "ACKategories", + "ACKategoriesTesting", + ] ), + .target( + name: "ACKategoriesTesting", + dependencies: [ + "ACKategories", + "Networking", + ] + ), + .target(name: "Networking"), + .testTarget( + name: "NetworkingTests", + dependencies: [ + "ACKategoriesTesting", + "Networking", + ] + ), + .target(name: "PushNotifications"), ] ) diff --git a/Sources/ACKategories/GradientView.swift b/Sources/ACKategories/UI/GradientView.swift similarity index 100% rename from Sources/ACKategories/GradientView.swift rename to Sources/ACKategories/UI/GradientView.swift diff --git a/Sources/ACKategories/UI/Popup/PopupModalAnimation.swift b/Sources/ACKategories/UI/Popup/PopupModalAnimation.swift new file mode 100644 index 00000000..cb5c2b3e --- /dev/null +++ b/Sources/ACKategories/UI/Popup/PopupModalAnimation.swift @@ -0,0 +1,147 @@ +#if canImport(UIKit) && !os(watchOS) +import Foundation +import UIKit + +/// Animation for presenting popups +public final class PopupModalAnimation: NSObject, UIViewControllerAnimatedTransitioning { + + /// Popup horizontal inset + public var popupXInset: CGFloat = 19 + + /// Background color for popup view + public var popupBackgroundColor: UIColor = .clear + + /// Background color for + public var backgroundColor: UIColor = .clear + + /// Visual effect for background view + public var visualEffect: UIVisualEffect? = UIBlurEffect(style: .light) + + /// Final alpha for background view + public var finalAlpha: CGFloat = 1.0 + + /// Duration of presenting modal + public var presentDuration: TimeInterval = 1.0 + + /// Duration of dismissing modal + public var dismissDuration: TimeInterval = 0.3 + + /// Shadow offset + public var shadowOffset: CGSize = CGSize.zero + + /// Shadow color + public var shadowColor: CGColor = UIColor.black.cgColor + + /// Shadow radius + public var shadowRadius: CGFloat = 5.0 + + /// Shadow opacity + public var shadowOpacity: Float = 0.5 + + /// Popup animation damping + public var animationDamping: CGFloat = 0.8 + + /// Initial animation spring velocity + public var animationInitialSpringVelocity: CGFloat = 1 + + public enum AnimationType { + case present + case dismiss + } + + public var animationType: AnimationType = .present + + private lazy var coverView: UIVisualEffectView = { + let view = UIVisualEffectView() + view.effect = visualEffect + view.backgroundColor = popupBackgroundColor + return view + }() + + public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + + //The view controller's view that is presenting the modal view + let containerView = transitionContext.containerView + containerView.translatesAutoresizingMaskIntoConstraints = false + + if animationType == .present { + //The modal view itself + guard let modalView = transitionContext.viewController(forKey: .to)?.view, let containerParent = containerView.superview else { return } + + modalView.translatesAutoresizingMaskIntoConstraints = false + + coverView.alpha = 0 + containerView.addSubview(coverView) + NSLayoutConstraint.activate([ + containerView.leadingAnchor.constraint(equalTo: containerParent.leadingAnchor), + containerView.trailingAnchor.constraint(equalTo: containerParent.trailingAnchor), + containerView.topAnchor.constraint(equalTo: containerParent.topAnchor), + containerView.bottomAnchor.constraint(equalTo: containerParent.bottomAnchor), + ]) + containerView.backgroundColor = backgroundColor + coverView.frame = containerView.frame + + containerView.addSubview(modalView) + NSLayoutConstraint.activate([ + modalView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: popupXInset), + modalView.heightAnchor.constraint(lessThanOrEqualTo: containerView.heightAnchor, multiplier: 0.85), + modalView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + modalView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + ]) + modalView.layer.contentsScale = UIScreen.main.scale + modalView.layer.shadowColor = shadowColor + modalView.layer.shadowOffset = shadowOffset + modalView.layer.shadowRadius = shadowRadius + modalView.layer.shadowOpacity = shadowOpacity + modalView.layer.masksToBounds = false + modalView.clipsToBounds = false + + let endFrame = modalView.frame + modalView.frame = CGRect(x: endFrame.origin.x, y: containerView.frame.size.height, width: endFrame.size.width, height: endFrame.size.height) + containerView.bringSubviewToFront(modalView) + + //Move off of the screen so we can slide it up + UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: animationDamping, initialSpringVelocity: animationInitialSpringVelocity, options: .curveLinear, animations: { + modalView.frame = endFrame + self.coverView.alpha = self.finalAlpha + self.coverView.effect = self.visualEffect + }, completion: { _ in + transitionContext.completeTransition(true) + }) + + } else if animationType == .dismiss { + guard let modalView = transitionContext.viewController(forKey: .from)?.view else { return } + //The modal view itself + UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: { + var frame = modalView.frame + frame.origin.y = containerView.frame.height + modalView.layer.transform = CATransform3DRotate(modalView.layer.transform, (3.14/180) * 35, 1, 0.0, 0.0) + self.coverView.alpha = 0 + self.coverView.effect = nil + + modalView.frame = frame + }, completion: { _ in + self.coverView.removeFromSuperview() + transitionContext.completeTransition(true) + }) + + } + } + + public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + animationType == .present ? presentDuration : dismissDuration + } +} + +extension PopupModalAnimation: UIViewControllerTransitioningDelegate { + public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + animationType = .present + return self + } + + public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + animationType = .dismiss + return self + } +} +#endif diff --git a/Sources/ACKategories/UI/Popup/PopupPresenting.swift b/Sources/ACKategories/UI/Popup/PopupPresenting.swift new file mode 100644 index 00000000..5f8bb969 --- /dev/null +++ b/Sources/ACKategories/UI/Popup/PopupPresenting.swift @@ -0,0 +1,18 @@ +#if canImport(UIKit) && !os(watchOS) +import UIKit + +public protocol PopupPresenting { + var popupAnimation: PopupModalAnimation { get } + + func present(popup: UIViewController) +} + +public extension PopupPresenting where Self: UIViewController { + /// Presents popup with animation + func present(popup: UIViewController, animated: Bool) { + popup.transitioningDelegate = popupAnimation + popup.modalPresentationStyle = .custom + present(popup, animated: animated) + } +} +#endif diff --git a/Sources/ACKategories/Reusable.swift b/Sources/ACKategories/UI/Reusable.swift similarity index 100% rename from Sources/ACKategories/Reusable.swift rename to Sources/ACKategories/UI/Reusable.swift diff --git a/Sources/ACKategories/ReusableView.swift b/Sources/ACKategories/UI/ReusableView.swift similarity index 100% rename from Sources/ACKategories/ReusableView.swift rename to Sources/ACKategories/UI/ReusableView.swift diff --git a/Sources/ACKategories/SelfSizingTableHeaderFooterView.swift b/Sources/ACKategories/UI/SelfSizingTableHeaderFooterView.swift similarity index 100% rename from Sources/ACKategories/SelfSizingTableHeaderFooterView.swift rename to Sources/ACKategories/UI/SelfSizingTableHeaderFooterView.swift diff --git a/Sources/ACKategories/TagListView.swift b/Sources/ACKategories/UI/TagListView.swift similarity index 100% rename from Sources/ACKategories/TagListView.swift rename to Sources/ACKategories/UI/TagListView.swift diff --git a/Sources/ACKategories/UI/Theme/ThemeExtensions.swift b/Sources/ACKategories/UI/Theme/ThemeExtensions.swift new file mode 100644 index 00000000..fa2094ca --- /dev/null +++ b/Sources/ACKategories/UI/Theme/ThemeExtensions.swift @@ -0,0 +1,9 @@ +#if canImport(UIKit) +import UIKit + +extension UIColor: ThemeProvider { } +#endif + +#if canImport(UIKit) && !os(watchOS) +extension UIView: ThemeProvider { } +#endif diff --git a/Sources/ACKategories/UI/Theme/ThemeProvider.swift b/Sources/ACKategories/UI/Theme/ThemeProvider.swift new file mode 100644 index 00000000..32db7713 --- /dev/null +++ b/Sources/ACKategories/UI/Theme/ThemeProvider.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct Theme { } + +public protocol ThemeProvider { } + +public extension ThemeProvider { + static var theme: Theme.Type { Theme.self } + + var theme: Theme { Theme() } // theoretically unneccessary allocation overhead every call, but SnapKit uses the same pattern so... +} diff --git a/Sources/ACKategories/UIApplicationExtensions.swift b/Sources/ACKategories/UI/UIApplicationExtensions.swift similarity index 100% rename from Sources/ACKategories/UIApplicationExtensions.swift rename to Sources/ACKategories/UI/UIApplicationExtensions.swift diff --git a/Sources/ACKategories/UILabelExtensions.swift b/Sources/ACKategories/UI/UILabelExtensions.swift similarity index 100% rename from Sources/ACKategories/UILabelExtensions.swift rename to Sources/ACKategories/UI/UILabelExtensions.swift diff --git a/Sources/ACKategories/UINavigationControllerExtensions.swift b/Sources/ACKategories/UI/UINavigationControllerExtensions.swift similarity index 100% rename from Sources/ACKategories/UINavigationControllerExtensions.swift rename to Sources/ACKategories/UI/UINavigationControllerExtensions.swift diff --git a/Sources/ACKategories/UISearchBarExtensions.swift b/Sources/ACKategories/UI/UISearchBarExtensions.swift similarity index 100% rename from Sources/ACKategories/UISearchBarExtensions.swift rename to Sources/ACKategories/UI/UISearchBarExtensions.swift diff --git a/Sources/ACKategories/UIStackViewExtensions.swift b/Sources/ACKategories/UI/UIStackViewExtensions.swift similarity index 100% rename from Sources/ACKategories/UIStackViewExtensions.swift rename to Sources/ACKategories/UI/UIStackViewExtensions.swift diff --git a/Sources/ACKategories/UIView+Spacer.swift b/Sources/ACKategories/UI/UIView+Spacer.swift similarity index 100% rename from Sources/ACKategories/UIView+Spacer.swift rename to Sources/ACKategories/UI/UIView+Spacer.swift diff --git a/Sources/ACKategories/UIViewController+Children.swift b/Sources/ACKategories/UI/UIViewController+Children.swift similarity index 100% rename from Sources/ACKategories/UIViewController+Children.swift rename to Sources/ACKategories/UI/UIViewController+Children.swift diff --git a/Sources/ACKategories/UIViewController+FrontMost.swift b/Sources/ACKategories/UI/UIViewController+FrontMost.swift similarity index 100% rename from Sources/ACKategories/UIViewController+FrontMost.swift rename to Sources/ACKategories/UI/UIViewController+FrontMost.swift diff --git a/Sources/ACKategories/UIViewExtensions.swift b/Sources/ACKategories/UI/UIViewExtensions.swift similarity index 100% rename from Sources/ACKategories/UIViewExtensions.swift rename to Sources/ACKategories/UI/UIViewExtensions.swift diff --git a/Sources/ACKategories/VersionUpdate/MinBuildNumberFetcher.swift b/Sources/ACKategories/VersionUpdate/MinBuildNumberFetcher.swift new file mode 100644 index 00000000..954e6582 --- /dev/null +++ b/Sources/ACKategories/VersionUpdate/MinBuildNumberFetcher.swift @@ -0,0 +1,6 @@ +import Foundation + +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public protocol MinBuildNumberFetcher { + var minBuildNumber: Int { get async throws } +} diff --git a/Sources/ACKategories/VersionUpdate/VersionUpdateManager.swift b/Sources/ACKategories/VersionUpdate/VersionUpdateManager.swift new file mode 100644 index 00000000..4fbee4aa --- /dev/null +++ b/Sources/ACKategories/VersionUpdate/VersionUpdateManager.swift @@ -0,0 +1,29 @@ +import Foundation + +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public protocol VersionUpdateManaging { + var updateRequired: Bool { get async } +} + +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public final class VersionUpdateManager: VersionUpdateManaging { + public var updateRequired: Bool { + get async { + guard let min = try? await fetcher.minBuildNumber else { return false } + return min > buildNumberProvider() + } + } + + private let buildNumberProvider: () -> Int + private let fetcher: MinBuildNumberFetcher + + public init( + buildNumberProvider: @escaping @autoclosure () -> Int = Bundle.main.infoDictionary + .flatMap { $0["CFBundleVersion"] as? String } + .flatMap(Int.init) ?? Int.max, + fetcher: MinBuildNumberFetcher + ) { + self.buildNumberProvider = buildNumberProvider + self.fetcher = fetcher + } +} diff --git a/Sources/ACKategoriesTesting/Networking/APIServiceMock.swift b/Sources/ACKategoriesTesting/Networking/APIServiceMock.swift new file mode 100644 index 00000000..d1417e2e --- /dev/null +++ b/Sources/ACKategoriesTesting/Networking/APIServiceMock.swift @@ -0,0 +1,41 @@ +import Foundation +import Networking + +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public final class APIService_Mock: APIServicing { + public init() { + + } + + public var requestBody: (URLRequest) async throws -> HTTPResponse = { + .init(request: $0, response: nil, data: nil) + } + + public func request(_ request: URLRequest) async throws -> HTTPResponse { + try await requestBody(request) + } + + public func request( + _ address: RequestAddress, + method: HTTPMethod, + query: [String: String]?, + headers: [String: String]?, + body: RequestBody? + ) async throws -> HTTPResponse { + let url: URL = { + switch address { + case .url(let url): return url + case .path(let path): + return .ackeeCZ.appendingPathComponent(path) + } + }() + + return try await request(.init( + url: url, + method: method, + query: query, + headers: headers, + body: body + )) + } +} diff --git a/Sources/ACKategoriesTesting/Networking/HTTPResponse+TestData.swift b/Sources/ACKategoriesTesting/Networking/HTTPResponse+TestData.swift new file mode 100644 index 00000000..cd7e033d --- /dev/null +++ b/Sources/ACKategoriesTesting/Networking/HTTPResponse+TestData.swift @@ -0,0 +1,16 @@ +import Foundation +import Networking + +public extension HTTPResponse { + static func test( + request: URLRequest = .test(), + response: HTTPURLResponse? = nil, + data: Data? = nil + ) -> HTTPResponse { + .init( + request: request, + response: response, + data: data + ) + } +} diff --git a/Sources/ACKategoriesTesting/Networking/HTTPURLResponse+TestData.swift b/Sources/ACKategoriesTesting/Networking/HTTPURLResponse+TestData.swift new file mode 100644 index 00000000..a806b0ac --- /dev/null +++ b/Sources/ACKategoriesTesting/Networking/HTTPURLResponse+TestData.swift @@ -0,0 +1,17 @@ +import Foundation + +public extension HTTPURLResponse { + static func test( + url: URL = .ackeeCZ, + statusCode: Int = 200, + httpVersion: String? = nil, + headerFields: [String: String]? = nil + ) -> HTTPURLResponse? { + .init( + url: url, + statusCode: statusCode, + httpVersion: httpVersion, + headerFields: headerFields + ) + } +} diff --git a/Sources/ACKategoriesTesting/Networking/NetworkMock.swift b/Sources/ACKategoriesTesting/Networking/NetworkMock.swift new file mode 100644 index 00000000..785c8c5a --- /dev/null +++ b/Sources/ACKategoriesTesting/Networking/NetworkMock.swift @@ -0,0 +1,15 @@ +import Foundation +import Networking + +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public final class Network_Mock: Networking { + public var requestBody: (URLRequest) async throws -> HTTPResponse = { _ in .test() } + + public init() { + + } + + public func request(_ request: URLRequest) async throws -> HTTPResponse { + try await requestBody(request) + } +} diff --git a/Sources/ACKategoriesTesting/Networking/URL+TestData.swift b/Sources/ACKategoriesTesting/Networking/URL+TestData.swift new file mode 100644 index 00000000..dfc5c417 --- /dev/null +++ b/Sources/ACKategoriesTesting/Networking/URL+TestData.swift @@ -0,0 +1,6 @@ +import Foundation + +public extension URL { + static var ackeeCZ = URL(string: "https://ackee.cz")! + static var ackeeDE = URL(string: "https://ackee.de")! +} diff --git a/Sources/ACKategoriesTesting/Networking/URLRequest+TestData.swift b/Sources/ACKategoriesTesting/Networking/URLRequest+TestData.swift new file mode 100644 index 00000000..23bc53fc --- /dev/null +++ b/Sources/ACKategoriesTesting/Networking/URLRequest+TestData.swift @@ -0,0 +1,12 @@ +import Foundation + +public extension URLRequest { + static func test( + url: URL = .ackeeCZ, + headers: [String: String]? = nil + ) -> URLRequest { + var request = URLRequest(url: url) + request.allHTTPHeaderFields = headers + return request + } +} diff --git a/Sources/ACKategoriesTesting/VersionUpdateFetcher_Mock.swift b/Sources/ACKategoriesTesting/VersionUpdateFetcher_Mock.swift new file mode 100644 index 00000000..7892c4ca --- /dev/null +++ b/Sources/ACKategoriesTesting/VersionUpdateFetcher_Mock.swift @@ -0,0 +1,10 @@ +import ACKategories + +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public final class VersionUpdateFetcher_Mock: MinBuildNumberFetcher { + public var minBuildNumber = 0 + + public init(minBuildNumber: Int = 0) { + self.minBuildNumber = minBuildNumber + } +} diff --git a/Sources/Networking/APIService.swift b/Sources/Networking/APIService.swift new file mode 100644 index 00000000..da83df68 --- /dev/null +++ b/Sources/Networking/APIService.swift @@ -0,0 +1,118 @@ +import Foundation + +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public final class APIService: APIServicing { + /// Error thrown when response status code is unexpected + public struct UnexpectedStatusCodeError: Error { + /// Received response with unexpected status code + public let response: HTTPResponse + + /// - Parameter response: Received response with unexpected status code + public init(response: HTTPResponse) { + self.response = response + } + } + + private let baseURLFactory: () -> URL + private let network: Networking + private let requestInterceptors: [RequestInterceptor] + private let responseInterceptors: [ResponseInterceptor] + + /// Create new `JSONAPIService` + /// - Parameters: + /// - baseURL: Base URL for requests that use path instead of full URL/request + /// - network: Network object performing API calls, `URLSession` basically + /// - requestInterceptors: List of request interceptors, useful for logging or header injections + /// - responseInterceptors: List of response interceptors, useful for logging or token refresh + public init( + baseURL: @autoclosure @escaping () -> URL, + network: Networking, + requestInterceptors: [RequestInterceptor] = [], + responseInterceptors: [ResponseInterceptor] = [] + ) { + self.baseURLFactory = baseURL + self.network = network + self.requestInterceptors = requestInterceptors + self.responseInterceptors = responseInterceptors + } + + public func request(_ originalRequest: URLRequest) async throws -> HTTPResponse { + var request = originalRequest + + for interceptor in requestInterceptors { + try await interceptor.intercept( + service: self, + request: &request + ) + } + + var response = try await network.request(request) + + for interceptor in responseInterceptors { + try await interceptor.intercept( + service: self, + response: &response + ) + } + + guard response.isAccepted() else { + throw UnexpectedStatusCodeError(response: response) + } + + return response + } + + public func request( + _ address: RequestAddress, + method: HTTPMethod = .get, + query: [String: String]? = nil, + headers: [String: String]? = nil, + body: RequestBody? = nil + ) async throws -> HTTPResponse { + let url: URL = { + switch address { + case .url(let url): return url + case .path(let path): + return baseURLFactory().appendingPathComponent(path) + } + }() + + return try await self.request(.init( + url: url, + method: method, + query: query, + headers: headers, + body: body + )) + } +} + +public extension URLRequest { + init( + url: URL, + method: HTTPMethod, + query: [String: String]?, + headers: [String: String]?, + body: RequestBody? + ) { + let url: URL = { + guard let query, !query.isEmpty else { return url } + + var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) + var queryItems = urlComponents?.queryItems ?? [] + query.forEach { key, value in + queryItems.append(.init(name: key, value: value)) + } + urlComponents?.queryItems = queryItems + + return urlComponents?.url ?? url + }() + + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + request.httpBody = body?.data + request.allHTTPHeaderFields = headers + request.setValue(body?.contentType, forHTTPHeaderField: "Content-Type") + self = request + } +} diff --git a/Sources/Networking/APIServicing.swift b/Sources/Networking/APIServicing.swift new file mode 100644 index 00000000..ead9797f --- /dev/null +++ b/Sources/Networking/APIServicing.swift @@ -0,0 +1,52 @@ +import Foundation + +/// Protocol wrapping objects that perform network requests +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public protocol APIServicing { + /// Send given `URLRequest` + /// - Parameter request: Request to be sent + /// - Returns: HTTPResponse to given request + func request(_ request: URLRequest) async throws -> HTTPResponse + + /// Construct and send request using given parameters + /// - Parameters: + /// - address: Request address + /// - method: Request method + /// - query: Query parameters + /// - headers: Custom headers + /// - body: Request body + /// - Returns: Received HTTP response + func request( + _ address: RequestAddress, + method: HTTPMethod, + query: [String: String]?, + headers: [String: String]?, + body: RequestBody? + ) async throws -> HTTPResponse +} + +/// Protocol wrapping interceptors that can modify requests before they are sent +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public protocol RequestInterceptor { + /// Intercept request sent by service + /// - Parameters: + /// - service: Service that wants to send given request + /// - request: Request that should be sent and can be modified + func intercept( + service: APIServicing, + request: inout URLRequest + ) async throws +} + +/// Protocol wrapping interceptors that can modify responses before they are returned from API service +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public protocol ResponseInterceptor { + /// Intercept response that was returned to service + /// - Parameters: + /// - service: Service that received given response + /// - response: Response that was received and can be modified + func intercept( + service: APIServicing, + response: inout HTTPResponse + ) async throws +} diff --git a/Sources/Networking/HTTPMethod.swift b/Sources/Networking/HTTPMethod.swift new file mode 100644 index 00000000..18cc2e05 --- /dev/null +++ b/Sources/Networking/HTTPMethod.swift @@ -0,0 +1,41 @@ +import Foundation + +public enum HTTPMethod { + case options + case get + case head + case post + case put + case patch + case delete + case trace + case connect + case custom(String) +} + +extension HTTPMethod: RawRepresentable { + public var rawValue: String { + switch self { + case .options: return "OPTIONS" + case .get: return "GET" + case .head: return "HEAD" + case .post: return "POST" + case .put: return "PUT" + case .patch: return "PATCH" + case .delete: return "DELETE" + case .trace: return "TRACE" + case .connect: return "CONNECT" + case .custom(let method): return method + } + } + + public init?(rawValue: String) { + self.init(stringLiteral: rawValue) + } +} + +extension HTTPMethod: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self = .custom(value) + } +} diff --git a/Sources/Networking/HTTPResponse.swift b/Sources/Networking/HTTPResponse.swift new file mode 100644 index 00000000..cceed3ba --- /dev/null +++ b/Sources/Networking/HTTPResponse.swift @@ -0,0 +1,28 @@ +import Foundation + +public struct HTTPResponse { + /// Request that was sent + public let request: URLRequest + /// Received `HTTPURLResponse` + public let response: HTTPURLResponse? + /// Body of response + public let data: Data? + /// Shortcut to response status code + public var statusCode: Int? { response?.statusCode } + + public init( + request: URLRequest, + response: HTTPURLResponse?, + data: Data? + ) { + self.request = request + self.response = response + self.data = data + } + + /// Check that `statusCode` is in accepted range (200...299 by default) + /// - Parameter acceptedStatusCodes: List of accepte status codes + public func isAccepted(acceptedStatusCodes codes: [Int] = .init(200...299)) -> Bool { + statusCode.map { codes.contains($0) } ?? true + } +} diff --git a/Sources/Networking/Networking.swift b/Sources/Networking/Networking.swift new file mode 100644 index 00000000..f054abe1 --- /dev/null +++ b/Sources/Networking/Networking.swift @@ -0,0 +1,23 @@ +import Foundation + +/// Protocol wrapping raw network requests, basically URLSession +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public protocol Networking { + /// Send given request + /// - Parameter request: Request to be sent + /// - Returns: Received response + func request(_ request: URLRequest) async throws -> HTTPResponse +} + +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 12.0, *) +extension URLSession: Networking { + public func request(_ request: URLRequest) async throws -> HTTPResponse { + let (data, response) = try await data(for: request) + + return .init( + request: request, + response: response as? HTTPURLResponse, + data: data + ) + } +} diff --git a/Sources/Networking/OAuthInterceptor.swift b/Sources/Networking/OAuthInterceptor.swift new file mode 100644 index 00000000..1c4bae25 --- /dev/null +++ b/Sources/Networking/OAuthInterceptor.swift @@ -0,0 +1,68 @@ +import Foundation + +/// Interceptor that is ready to solve token refresh +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +public final actor OAuthInterceptor: ResponseInterceptor { + /// Enum for results when checking if request current auth data + public enum UsedCurrentAuthData { + /// Request used current auth data + case yes + /// Request did not use current auth data and should use + /// given URLRequest to retry + case no(URLRequest) + } + + /// Determines if given response is error due to expired auth data (auth token basically) + public let isExpiredAuthDataResponse: (HTTPResponse) async -> Bool + /// Determines if given response and its request used current auth data (auth token basically). + /// + /// Based on that `refeshAuthData` could be called. + public let requestUsedCurrentAuthData: (HTTPResponse) async -> OAuthInterceptor.UsedCurrentAuthData + + /// Closure that refreshes auth data (auth token basically) and returns a function that transforms received response (and its request) + /// to new request so we can retry that request again with new auth data + public let refreshAuthData: () async throws -> (HTTPResponse) -> (URLRequest) + + /// To prevent actor [re-entrancy issue](https://medium.com/@mark.moeykens/swift-actor-reentrancy-model-explained-11463d993c59) + /// we track call to `refreshAuthData()` closure as it should never occur multiple times at once + private var refreshTask: Task<(HTTPResponse) -> (URLRequest), Error>? + + /// Create new interceptor + /// - Parameters: + /// - isExpiredAuthDataResponse: Determines if given response is error due to expired auth data (auth token basically) + /// - requestUsedCurrentAuthData: Determines if given response and its request used current auth data (auth token basically). Based on that `refeshAuthData` could be called. + /// - refreshAuthData: Closure that refreshes auth data (auth token basically) and returns a function that transforms received response (and its request) to new request so we can retry that request again with new auth data + public init( + isExpiredAuthDataResponse: @escaping (HTTPResponse) async -> Bool, + requestUsedCurrentAuthData: @escaping (HTTPResponse) async -> UsedCurrentAuthData, + refreshAuthData: @escaping () async throws -> (HTTPResponse) -> (URLRequest) + ) { + self.isExpiredAuthDataResponse = isExpiredAuthDataResponse + self.requestUsedCurrentAuthData = requestUsedCurrentAuthData + self.refreshAuthData = refreshAuthData + } + + public func intercept( + service: APIServicing, + response originalResponse: inout HTTPResponse + ) async throws { + guard await isExpiredAuthDataResponse(originalResponse) else { + return + } + + switch await requestUsedCurrentAuthData(originalResponse) { + case .yes: + let task = refreshTask ?? .init { + defer { refreshTask = nil } + return try await refreshAuthData() + } + + refreshTask = task + + let newRequest = try await task.value(originalResponse) + originalResponse = try await service.request(newRequest) + case .no(let newRequest): + originalResponse = try await service.request(newRequest) + } + } +} diff --git a/Sources/Networking/RequestAddress.swift b/Sources/Networking/RequestAddress.swift new file mode 100644 index 00000000..eb5d4f25 --- /dev/null +++ b/Sources/Networking/RequestAddress.swift @@ -0,0 +1,22 @@ +import Foundation + +/// Represents request address that could be relative to service's base URL +/// +/// If initialized by string literal/interpolation, respective case is created, +/// if Foundation.URL can be created and scheme is not empty, it creates `.url` otherwise `.path` +public enum RequestAddress: Hashable { + case url(URL) + case path(String) +} + +extension RequestAddress: ExpressibleByStringInterpolation { + public init(stringLiteral value: String) { + if let url = URL(string: value), + let scheme = url.scheme, + !scheme.isEmpty { + self = .url(url) + } else { + self = .path(value) + } + } +} diff --git a/Sources/Networking/RequestBody.swift b/Sources/Networking/RequestBody.swift new file mode 100644 index 00000000..2161d798 --- /dev/null +++ b/Sources/Networking/RequestBody.swift @@ -0,0 +1,99 @@ +import Foundation + +public protocol Encoder: AnyObject { + /// Content type to which encoder encodes data + /// + /// Used for `Content-Type` header + static var contentType: String { get } + + func encode(_ encodable: T) throws -> Data +} + +extension JSONEncoder: Encoder { + public static var contentType: String { "application/json" } +} + +public struct RequestBody { + public let contentType: String + public let data: Data + + public init( + contentType: String, + data: Data + ) { + self.contentType = contentType + self.data = data + } + + public init(_ encodable: T, encoder: Encoder) throws { + contentType = type(of: encoder).contentType + data = try encoder.encode(encodable) + } + + public init( + jsonDictionary dict: [String: Any], + options: JSONSerialization.WritingOptions = [ + .prettyPrinted, + .sortedKeys, + ] + ) throws { + try self.init(jsonObject: dict, options: options) + } + + public init( + jsonArray array: [Any], + options: JSONSerialization.WritingOptions = [ + .prettyPrinted, + .sortedKeys, + ] + ) throws { + try self.init(jsonObject: array, options: options) + } + + public init(formURLEncoded dict: [String: String]) throws { + let data = dict.compactMap { key, value in + guard !key.isEmpty, !value.isEmpty, + let encodedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let encodedValue = key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + else { return nil } + + return encodedKey + "=" + encodedValue + } + .joined(separator: "&") + .data(using: .utf8) + + guard let data else { + struct CannotEncodeData: Error { } + throw CannotEncodeData() + } + + self.data = data + self.contentType = "application/x-www-form-urlencoded" + } + + private init( + jsonObject: Any, + options: JSONSerialization.WritingOptions = [ + .prettyPrinted, + .sortedKeys, + ] + ) throws { + data = try JSONSerialization.data( + withJSONObject: jsonObject, + options: options + ) + contentType = JSONEncoder.contentType + } +} + +extension RequestBody: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, Any)...) { + try! self.init(jsonDictionary: .init(elements) { $1 }) + } +} + +extension RequestBody: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: Any...) { + try! self.init(jsonArray: elements) + } +} diff --git a/Sources/PushNotifications/PushManager.swift b/Sources/PushNotifications/PushManager.swift new file mode 100644 index 00000000..93452514 --- /dev/null +++ b/Sources/PushNotifications/PushManager.swift @@ -0,0 +1,142 @@ +import Foundation +import UserNotifications + +@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) +public protocol PushManaging { + var actions: PushManagingActions { get } + + var notificationSettings: AsyncStream { get } + var currentNotificationSettings: UNNotificationSettings? { get } +} + +@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) +public protocol PushManagingActions { + func start() + func requestPermission(options: UNAuthorizationOptions) async + + func addNotificationReceivedHandler(_ handler: @escaping (UNNotification) async -> ()) -> String + func removeNotificationReceivedHandler(id: String) + + #if !os(tvOS) + func addNotificationOpenedHandler(_ handler: @escaping (UNNotificationResponse) async -> ()) -> String + func removeNotificationOpenedHandler(id: String) + #endif +} + +@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) +public extension PushManaging where Self: PushManagingActions { + var actions: PushManagingActions { self } +} + +@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) +public final class PushManager: NSObject, PushManaging, PushManagingActions { + public private(set) lazy var notificationSettings = AsyncStream { continuation in + notificationSettingsContinuation = continuation + + if let currentNotificationSettings { + continuation.yield(currentNotificationSettings) + } + } + public private(set) var currentNotificationSettings: UNNotificationSettings? + + public lazy var presentationOptions: (UNNotification) async -> UNNotificationPresentationOptions = { [weak self] notification in + guard let self else { return [] } + return await notificationCenter.notificationSettings().allowedPresentationOptions + } + + public var openSettings: (UNNotification?) -> () = { _ in } + + private let notificationCenter: UNUserNotificationCenter + private var notificationSettingsContinuation: AsyncStream.Continuation? + + private var notificationReceivedHandlers = [String: (UNNotification) async -> ()]() + + #if !os(tvOS) + private var notificationOpenedHandlers = [String: (UNNotificationResponse) async -> ()]() + #endif + + public init( + notificationCenter: UNUserNotificationCenter = .current() + ) { + self.notificationCenter = notificationCenter + super.init() + } + + deinit { + notificationSettingsContinuation?.finish() + } + + public func start() { + notificationCenter.delegate = self + } + + public func requestPermission(options: UNAuthorizationOptions) async { + guard let granted = try? await notificationCenter.requestAuthorization(options: options), + granted + else { return } + + let settings = await notificationCenter.notificationSettings() + currentNotificationSettings = settings + notificationSettingsContinuation?.yield(settings) + } + + public func addNotificationReceivedHandler( + _ handler: @escaping (UNNotification) async -> () + ) -> String { + let id = UUID().uuidString + notificationReceivedHandlers[id] = handler + return id + } + + public func removeNotificationReceivedHandler(id: String) { + notificationReceivedHandlers[id] = nil + } + + #if !os(tvOS) + public func addNotificationOpenedHandler( + _ handler: @escaping (UNNotificationResponse) async -> () + ) -> String { + let id = UUID().uuidString + notificationOpenedHandlers[id] = handler + return id + } + + public func removeNotificationOpenedHandler(id: String) { + notificationOpenedHandlers[id] = nil + } + #endif +} + +@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) +extension PushManager: UNUserNotificationCenterDelegate { + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification + ) async -> UNNotificationPresentationOptions { + for handler in notificationReceivedHandlers.values { + await handler(notification) + } + + return await presentationOptions(notification) + } + + #if !os(tvOS) + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse + ) async { + for handler in notificationOpenedHandlers.values { + await handler(response) + } + } + #endif + + #if !os(watchOS) && !os(tvOS) + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + openSettingsFor notification: UNNotification? + ) { + openSettings(notification) + } + #endif +} diff --git a/Sources/PushNotifications/UNNotificationSettingsExtensions.swift b/Sources/PushNotifications/UNNotificationSettingsExtensions.swift new file mode 100644 index 00000000..07492ae8 --- /dev/null +++ b/Sources/PushNotifications/UNNotificationSettingsExtensions.swift @@ -0,0 +1,22 @@ +import UserNotifications + +@available(iOS 13.0, macOS 10.15, *) +public extension UNNotificationSettings { + var allowedPresentationOptions: UNNotificationPresentationOptions { + var options = [(UNNotificationSetting, UNNotificationPresentationOptions)]() + + #if !os(tvOS) + options.append((soundSetting, UNNotificationPresentationOptions.sound)) + options.append((alertSetting, .alert)) + #endif + + #if !os(watchOS) + options.append((badgeSetting, .badge)) + #endif + + return .init(options.compactMap { setting, option in + if setting == .enabled { return option } + return nil + }) + } +} diff --git a/Tests/ACKategoriesTests/EdgeInsetsTests.swift b/Tests/ACKategoriesTests/EdgeInsetsTests.swift index 9516013b..554e710b 100644 --- a/Tests/ACKategoriesTests/EdgeInsetsTests.swift +++ b/Tests/ACKategoriesTests/EdgeInsetsTests.swift @@ -2,6 +2,7 @@ import ACKategories import SwiftUI import XCTest +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) final class EdgeInsetsTests: XCTestCase { func test_init_size() { XCTAssertEqual( diff --git a/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift b/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift new file mode 100644 index 00000000..1a80f4c3 --- /dev/null +++ b/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift @@ -0,0 +1,58 @@ +import ACKategories +import ACKategoriesTesting +import XCTest + +@MainActor +@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) +final class VersionUpdateManager_Tests: XCTestCase { + private var fetcher: VersionUpdateFetcher_Mock! + + // MARK: - Setup + + override func setUp() { + fetcher = .init() + super.setUp() + } + + func test_minBuildNumber_lower() async throws { + let buildNumber = Int.random(in: 1..<10_000) + + fetcher.minBuildNumber = buildNumber - 1 + + let subject = VersionUpdateManager( + buildNumberProvider: buildNumber, + fetcher: fetcher + ) + let updateRequired = await subject.updateRequired + + XCTAssertFalse(updateRequired) + } + + func test_minBuildNumber_equal() async throws { + let buildNumber = Int.random(in: 1..<10_000) + + fetcher.minBuildNumber = buildNumber + + let subject = VersionUpdateManager( + buildNumberProvider: buildNumber, + fetcher: fetcher + ) + let updateRequired = await subject.updateRequired + + XCTAssertFalse(updateRequired) + } + + func test_minBuildNumber_higher() async throws { + let buildNumber = Int.random(in: 1..<10_000) + + fetcher.minBuildNumber = buildNumber + 1 + + let subject = VersionUpdateManager( + buildNumberProvider: buildNumber, + fetcher: fetcher + ) + let updateRequired = await subject.updateRequired + + XCTAssertTrue(updateRequired) + } +} diff --git a/Tests/NetworkingTests/APIService+OAuthInterceptor_Tests.swift b/Tests/NetworkingTests/APIService+OAuthInterceptor_Tests.swift new file mode 100644 index 00000000..e5a599cb --- /dev/null +++ b/Tests/NetworkingTests/APIService+OAuthInterceptor_Tests.swift @@ -0,0 +1,176 @@ +import ACKategoriesTesting +import Networking +import XCTest + +@MainActor +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +final class APIService_OAuthInterceptor_IntegrationTests: XCTestCase { + private var network: Network_Mock! + + // MARK: - Setup + + override func setUp() { + network = .init() + super.setUp() + } + + // MARK: - Tests + + func test_expiredTokenRefresh() async throws { + let originalToken = "abc" + let newToken = "def" + let authHeaderName = "Authorization" + var currentToken = originalToken + + let refreshResponseRequest: (HTTPResponse) -> URLRequest = { response in + var request = response.request + request.setValue(currentToken, forHTTPHeaderField: authHeaderName) + return request + } + + let oauth = OAuthInterceptor( + isExpiredAuthDataResponse: { $0.statusCode == 401 }, + requestUsedCurrentAuthData: { + let isCurrent = $0.request.value(forHTTPHeaderField: authHeaderName) == currentToken + guard !isCurrent else { + return .yes + } + + return .no(refreshResponseRequest($0)) + }, + refreshAuthData: { + currentToken = newToken + + self.network.requestBody = { + .test( + request: $0, + response: .init( + url: try XCTUnwrap($0.url), + statusCode: 200, + httpVersion: nil, + headerFields: nil + ) + ) + } + + return refreshResponseRequest + } + ) + let apiService = APIService( + baseURL: .ackeeCZ, + network: network, + responseInterceptors: [oauth] + ) + + network.requestBody = { + .test( + request: $0, + response: .init( + url: try XCTUnwrap($0.url), + statusCode: 401, + httpVersion: nil, + headerFields: nil + ) + ) + } + + let response = try await apiService.request(.test( + url: .ackeeCZ, + headers: ["Authorization": originalToken] + )) + + XCTAssertEqual(newToken, response.request.value(forHTTPHeaderField: authHeaderName)) + } + + func test_expiredTokenRefresh_concurrent() async throws { + let originalToken = "abc" + let newToken = "def" + let authHeaderName = "Authorization" + let taskHeaderName = "Task" + var currentToken = originalToken + var refreshCount = 0 + var retriedRequests = [URLRequest]() + + let refreshResponseRequest: (HTTPResponse) -> URLRequest = { response in + var request = response.request + request.setValue(currentToken, forHTTPHeaderField: authHeaderName) + return request + } + + let oauth = OAuthInterceptor( + isExpiredAuthDataResponse: { $0.statusCode == 401 }, + requestUsedCurrentAuthData: { + let isCurrent = $0.request.value(forHTTPHeaderField: authHeaderName) == currentToken + guard !isCurrent else { + return .yes + } + + return .no(refreshResponseRequest($0)) + }, + refreshAuthData: { + refreshCount += 1 + currentToken = newToken + + self.network.requestBody = { + try await Task.sleep(nanoseconds: 1_000_000) + retriedRequests.append($0) + return .test( + request: $0, + response: .init( + url: try XCTUnwrap($0.url), + statusCode: 200, + httpVersion: nil, + headerFields: nil + ) + ) + } + + return refreshResponseRequest + } + ) + let apiService = APIService( + baseURL: .ackeeCZ, + network: network, + responseInterceptors: [oauth] + ) + + network.requestBody = { + .test( + request: $0, + response: .init( + url: try XCTUnwrap($0.url), + statusCode: 401, + httpVersion: nil, + headerFields: nil + ) + ) + } + + let taskCount = Int.random(in: 10...300) + let exp = expectation(description: "Refresh expectation") + exp.expectedFulfillmentCount = taskCount + + for i in 0.. URLRequest = { response in + var request = response.request + var headers = request.allHTTPHeaderFields! + headers["refreshed"] = "true" + request.allHTTPHeaderFields = headers + return request + } + + let subject = OAuthInterceptor( + isExpiredAuthDataResponse: { response in + let request = response.request + print("[IS_EXPIRED]", request.allHTTPHeaderFields!["index"]!, request.allHTTPHeaderFields!["refreshed"] ?? "false") + return true + }, + requestUsedCurrentAuthData: { response in + guard response.request.allHTTPHeaderFields?["index"] == "0" else { + return response.request.allHTTPHeaderFields?["refreshed"] == "true" ? .yes : .no(refreshResponseRequest(response)) + } + + return .yes + }, + refreshAuthData: { + refreshCount += 1 + return { response in + let request = response.request + print("[REFRESH]", request.allHTTPHeaderFields!["index"]!, request.allHTTPHeaderFields!["refreshed"] ?? "false") + return refreshResponseRequest(response) + } + } + ) + + service.requestBody = { request in + print("[REQUEST][RETRY]", request.allHTTPHeaderFields!["index"]!, request.allHTTPHeaderFields!["refreshed"] ?? "false") + try await Task.sleep(nanoseconds: 1_000_000_000) + return .test() + } + + let taskCount = Int.random(in: 10...300) + let exp = expectation(description: "Refresh expectation") + exp.expectedFulfillmentCount = taskCount + + for i in 0..