├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ ├── test-production.yml │ └── test-staging.yml ├── .gitignore ├── .swiftlint.yml ├── Gemfile ├── PokemonCards.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── WorkspaceSettings.xcsettings │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── Production.xcscheme │ └── Staging.xcscheme ├── PokemonCards ├── Application │ ├── CardDetail │ │ ├── CardDetailCore.swift │ │ └── CardDetailView.swift │ ├── Cards │ │ ├── CardsCore.swift │ │ └── CardsView.swift │ ├── Favorites │ │ ├── FavoritesCore.swift │ │ └── FavoritesView.swift │ ├── Launch │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── SceneDelegate.swift │ │ ├── en.lproj │ │ │ └── LaunchScreen.strings │ │ └── pt-PT.lproj │ │ │ └── LaunchScreen.strings │ └── Main │ │ ├── MainCore.swift │ │ └── MainView.swift ├── Data │ ├── Client │ │ ├── CardsClient.swift │ │ ├── FavoriteCardsClient.swift │ │ └── Provider │ │ │ ├── CoreData │ │ │ ├── CoreData.swift │ │ │ ├── FavoriteCard+Utility.swift │ │ │ └── PokemonCards.xcdatamodeld │ │ │ │ └── Cards.xcdatamodel │ │ │ │ └── contents │ │ │ ├── Provider+Cards.swift │ │ │ ├── Provider+FavoriteCards.swift │ │ │ ├── Provider.swift │ │ │ ├── ProviderError.swift │ │ │ └── Router │ │ │ ├── Router+Cards.swift │ │ │ └── Router.swift │ └── Model │ │ └── Cards.swift ├── Design System │ ├── Components │ │ ├── Buttons │ │ │ └── FavoriteButton.swift │ │ └── Items │ │ │ └── CardItemView.swift │ └── Representables │ │ └── ActivityIndicator.swift ├── Localization │ ├── Localization+Cards.swift │ ├── Localization+Favorites.swift │ └── Localization.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Configs │ │ ├── Production.xcconfig │ │ └── Staging.xcconfig │ ├── Info.plist │ ├── Localization │ │ ├── en.lproj │ │ │ └── Localizable.strings │ │ └── pt-PT.lproj │ │ │ └── Localizable.strings │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json └── Utility │ ├── Environment.swift │ ├── Error+Equality.swift │ └── UUID+Increment.swift ├── PokemonCardsSnapshotTests ├── Info.plist ├── PokemonCardsSnapshotTests.swift └── __Snapshots__ │ └── PokemonCardsSnapshotTests │ ├── testCardDetail.1.png │ ├── testCardsList.1.png │ └── testCardsList.2.png ├── PokemonCardsTests ├── Info.plist └── PokemonCardsTests.swift ├── README.md ├── fastlane ├── Appfile ├── Fastfile └── README.md └── swiftformat /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code Owners 2 | # 3 | # People in charge of the code and reviewing the PRs 4 | * @rageofflames @dfalmeida 5 | 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Story 2 | Jira, Notion, (link to where the story you worked on lives) 3 | 4 | ## Description 5 | A few sentences describing the overall goals of the pull request's commits. 6 | 7 | ## Impacted Areas in Application 8 | List general components of the application that this PR will affect: 9 | * 10 | 11 | ## Reviewer checklist: 12 | - [ ] Automated Tests 13 | - [ ] Manual Tests 14 | - [ ] Documentation 15 | - [ ] Versioning -------------------------------------------------------------------------------- /.github/workflows/test-production.yml: -------------------------------------------------------------------------------- 1 | name: Test Production 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Start xcodebuild test 15 | run: | 16 | fastlane test_production 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/test-staging.yml: -------------------------------------------------------------------------------- 1 | name: Test Staging 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-11 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Start xcodebuild test 15 | run: | 16 | fastlane test_staging 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - Pods 3 | disabled_rules: 4 | - trailing_whitespace 5 | - identifier_name 6 | line_length: 150 7 | force_cast: error 8 | force_try: 9 | severity: error 10 | warning_threshold: 1 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /PokemonCards.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1004FECA25794FD7008A6797 /* CardDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1004FEC925794FD7008A6797 /* CardDetailView.swift */; }; 11 | 1004FED1257957AD008A6797 /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1004FED0257957AD008A6797 /* Localization.swift */; }; 12 | 1004FED7257957D1008A6797 /* Localization+Cards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1004FED6257957D1008A6797 /* Localization+Cards.swift */; }; 13 | 1004FEE625795A5C008A6797 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1004FEE825795A5C008A6797 /* Localizable.strings */; }; 14 | 1004FEF025795EE6008A6797 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1004FEEF25795EE6008A6797 /* ActivityIndicator.swift */; }; 15 | 101D632D257A94BE0082D944 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 101D632C257A94BE0082D944 /* FavoriteButton.swift */; }; 16 | 102A463D258BCC8D00301BC7 /* FavoritesCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10976A44258BCB0300CC4355 /* FavoritesCore.swift */; }; 17 | 102A4641258BCD0300301BC7 /* FavoritesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10976A45258BCB0300CC4355 /* FavoritesView.swift */; }; 18 | 102A4649258BD05E00301BC7 /* Localization+Favorites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 102A4648258BD05E00301BC7 /* Localization+Favorites.swift */; }; 19 | 108C41342583BA5D00615793 /* FavoriteCardsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108C41332583BA5D00615793 /* FavoriteCardsClient.swift */; }; 20 | 108C413E2583D5C100615793 /* UUID+Increment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108C413D2583D5C100615793 /* UUID+Increment.swift */; }; 21 | 10976A25258BAC9F00CC4355 /* FavoriteCard+Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10976A24258BAC9F00CC4355 /* FavoriteCard+Utility.swift */; }; 22 | 10976A31258BC68F00CC4355 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10976A30258BC68F00CC4355 /* MainView.swift */; }; 23 | 10976A36258BC69F00CC4355 /* MainCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10976A35258BC69F00CC4355 /* MainCore.swift */; }; 24 | 10B2D6AB258381BC00A9C65D /* PokemonCards.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 10B2D6A9258381BC00A9C65D /* PokemonCards.xcdatamodeld */; }; 25 | 10B2D6AF258384AB00A9C65D /* Provider+FavoriteCards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10B2D6AE258384AB00A9C65D /* Provider+FavoriteCards.swift */; }; 26 | 10B2D6B725838CD200A9C65D /* CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10B2D6B625838CD200A9C65D /* CoreData.swift */; }; 27 | 10CD45B823C4E58600E0D0D4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CD45B723C4E58600E0D0D4 /* AppDelegate.swift */; }; 28 | 10CD45BA23C4E58600E0D0D4 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CD45B923C4E58600E0D0D4 /* SceneDelegate.swift */; }; 29 | 10CD45BE23C4E58700E0D0D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 10CD45BD23C4E58700E0D0D4 /* Assets.xcassets */; }; 30 | 10CD45C123C4E58700E0D0D4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 10CD45C023C4E58700E0D0D4 /* Preview Assets.xcassets */; }; 31 | 10CD45C423C4E58700E0D0D4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 10CD45C223C4E58700E0D0D4 /* LaunchScreen.storyboard */; }; 32 | 10D3D069257799120025C730 /* Cards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D3D068257799120025C730 /* Cards.swift */; }; 33 | 10D3D0822577B0DF0025C730 /* CardDetailCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D3D0812577B0DF0025C730 /* CardDetailCore.swift */; }; 34 | 10D3D0882577B6270025C730 /* CardItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D3D0872577B6270025C730 /* CardItemView.swift */; }; 35 | 10D3D0922577B9AC0025C730 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 10D3D0912577B9AC0025C730 /* KingfisherSwiftUI */; }; 36 | 10D63496258A72EF009CB28D /* PokemonCardsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D63495258A72EF009CB28D /* PokemonCardsSnapshotTests.swift */; }; 37 | 10D6349D258A7321009CB28D /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 10D6349C258A7321009CB28D /* SnapshotTesting */; }; 38 | 10E379A1256FFE5800B2CFBE /* Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E379A0256FFE5800B2CFBE /* Provider.swift */; }; 39 | 10E379AF2570034100B2CFBE /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E379AE2570034100B2CFBE /* Router.swift */; }; 40 | 10E379E2257007A200B2CFBE /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E379E1257007A200B2CFBE /* Environment.swift */; }; 41 | 10E379E725700AEC00B2CFBE /* Router+Cards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E379E625700AEC00B2CFBE /* Router+Cards.swift */; }; 42 | 10E379EC25700CCB00B2CFBE /* Provider+Cards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E379EB25700CCB00B2CFBE /* Provider+Cards.swift */; }; 43 | 10E379F625701A5700B2CFBE /* CardsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E379F525701A5700B2CFBE /* CardsView.swift */; }; 44 | 10E37A0225701B2B00B2CFBE /* CardsCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E37A0125701B2B00B2CFBE /* CardsCore.swift */; }; 45 | 10E4047325706B0900B1FA1C /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 10E4047225706B0900B1FA1C /* ComposableArchitecture */; }; 46 | 10E404B425706D7C00B1FA1C /* PokemonCardsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E404B225706D7C00B1FA1C /* PokemonCardsTests.swift */; }; 47 | 10F6F4F625710784001851A5 /* Error+Equality.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F6F4F525710784001851A5 /* Error+Equality.swift */; }; 48 | 10F6F4FC257108B2001851A5 /* ProviderError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F6F4FB257108B2001851A5 /* ProviderError.swift */; }; 49 | 10F6F516257125EF001851A5 /* CardsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F6F515257125EF001851A5 /* CardsClient.swift */; }; 50 | /* End PBXBuildFile section */ 51 | 52 | /* Begin PBXContainerItemProxy section */ 53 | 10D63478258A72A3009CB28D /* PBXContainerItemProxy */ = { 54 | isa = PBXContainerItemProxy; 55 | containerPortal = 10CD45AC23C4E58600E0D0D4 /* Project object */; 56 | proxyType = 1; 57 | remoteGlobalIDString = 10CD45B323C4E58600E0D0D4; 58 | remoteInfo = PokemonCards; 59 | }; 60 | 10E404AA25706D6900B1FA1C /* PBXContainerItemProxy */ = { 61 | isa = PBXContainerItemProxy; 62 | containerPortal = 10CD45AC23C4E58600E0D0D4 /* Project object */; 63 | proxyType = 1; 64 | remoteGlobalIDString = 10CD45B323C4E58600E0D0D4; 65 | remoteInfo = PokemonCards; 66 | }; 67 | /* End PBXContainerItemProxy section */ 68 | 69 | /* Begin PBXFileReference section */ 70 | 1004FEC925794FD7008A6797 /* CardDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardDetailView.swift; sourceTree = ""; }; 71 | 1004FED0257957AD008A6797 /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; 72 | 1004FED6257957D1008A6797 /* Localization+Cards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Cards.swift"; sourceTree = ""; }; 73 | 1004FEE725795A5C008A6797 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 74 | 1004FEEB25795A61008A6797 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; 75 | 1004FEEF25795EE6008A6797 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; 76 | 101D632C257A94BE0082D944 /* FavoriteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteButton.swift; sourceTree = ""; }; 77 | 102A4648258BD05E00301BC7 /* Localization+Favorites.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Favorites.swift"; sourceTree = ""; }; 78 | 108C41332583BA5D00615793 /* FavoriteCardsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteCardsClient.swift; sourceTree = ""; }; 79 | 108C413D2583D5C100615793 /* UUID+Increment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UUID+Increment.swift"; sourceTree = ""; }; 80 | 10976A24258BAC9F00CC4355 /* FavoriteCard+Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteCard+Utility.swift"; sourceTree = ""; }; 81 | 10976A30258BC68F00CC4355 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 82 | 10976A35258BC69F00CC4355 /* MainCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCore.swift; sourceTree = ""; }; 83 | 10976A44258BCB0300CC4355 /* FavoritesCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesCore.swift; sourceTree = ""; }; 84 | 10976A45258BCB0300CC4355 /* FavoritesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesView.swift; sourceTree = ""; }; 85 | 10B2D6AA258381BC00A9C65D /* Cards.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Cards.xcdatamodel; sourceTree = ""; }; 86 | 10B2D6AE258384AB00A9C65D /* Provider+FavoriteCards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Provider+FavoriteCards.swift"; sourceTree = ""; }; 87 | 10B2D6B625838CD200A9C65D /* CoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreData.swift; sourceTree = ""; }; 88 | 10CD45B423C4E58600E0D0D4 /* PokemonCards.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PokemonCards.app; sourceTree = BUILT_PRODUCTS_DIR; }; 89 | 10CD45B723C4E58600E0D0D4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 90 | 10CD45B923C4E58600E0D0D4 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 91 | 10CD45BD23C4E58700E0D0D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 92 | 10CD45C023C4E58700E0D0D4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 93 | 10CD45C323C4E58700E0D0D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 94 | 10CD45C523C4E58700E0D0D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 95 | 10D3D068257799120025C730 /* Cards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cards.swift; sourceTree = ""; }; 96 | 10D3D0812577B0DF0025C730 /* CardDetailCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardDetailCore.swift; sourceTree = ""; }; 97 | 10D3D0872577B6270025C730 /* CardItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardItemView.swift; sourceTree = ""; }; 98 | 10D63473258A72A3009CB28D /* PokemonCardsSnapshotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PokemonCardsSnapshotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 99 | 10D63477258A72A3009CB28D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 100 | 10D63495258A72EF009CB28D /* PokemonCardsSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokemonCardsSnapshotTests.swift; sourceTree = ""; }; 101 | 10E379A0256FFE5800B2CFBE /* Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Provider.swift; sourceTree = ""; }; 102 | 10E379AE2570034100B2CFBE /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; 103 | 10E379B52570040B00B2CFBE /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = ""; }; 104 | 10E379B62570049A00B2CFBE /* Staging.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Staging.xcconfig; sourceTree = ""; }; 105 | 10E379E1257007A200B2CFBE /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; 106 | 10E379E625700AEC00B2CFBE /* Router+Cards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Router+Cards.swift"; sourceTree = ""; }; 107 | 10E379EB25700CCB00B2CFBE /* Provider+Cards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Provider+Cards.swift"; sourceTree = ""; }; 108 | 10E379F525701A5700B2CFBE /* CardsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsView.swift; sourceTree = ""; }; 109 | 10E37A0125701B2B00B2CFBE /* CardsCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsCore.swift; sourceTree = ""; }; 110 | 10E404A525706D6900B1FA1C /* PokemonCardsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PokemonCardsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 111 | 10E404B225706D7C00B1FA1C /* PokemonCardsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokemonCardsTests.swift; sourceTree = ""; }; 112 | 10E404B325706D7C00B1FA1C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 113 | 10F6F4F525710784001851A5 /* Error+Equality.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Equality.swift"; sourceTree = ""; }; 114 | 10F6F4FB257108B2001851A5 /* ProviderError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderError.swift; sourceTree = ""; }; 115 | 10F6F515257125EF001851A5 /* CardsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsClient.swift; sourceTree = ""; }; 116 | /* End PBXFileReference section */ 117 | 118 | /* Begin PBXFrameworksBuildPhase section */ 119 | 10CD45B123C4E58600E0D0D4 /* Frameworks */ = { 120 | isa = PBXFrameworksBuildPhase; 121 | buildActionMask = 2147483647; 122 | files = ( 123 | 10D3D0922577B9AC0025C730 /* KingfisherSwiftUI in Frameworks */, 124 | 10E4047325706B0900B1FA1C /* ComposableArchitecture in Frameworks */, 125 | ); 126 | runOnlyForDeploymentPostprocessing = 0; 127 | }; 128 | 10D63470258A72A3009CB28D /* Frameworks */ = { 129 | isa = PBXFrameworksBuildPhase; 130 | buildActionMask = 2147483647; 131 | files = ( 132 | 10D6349D258A7321009CB28D /* SnapshotTesting in Frameworks */, 133 | ); 134 | runOnlyForDeploymentPostprocessing = 0; 135 | }; 136 | 10E404A225706D6900B1FA1C /* Frameworks */ = { 137 | isa = PBXFrameworksBuildPhase; 138 | buildActionMask = 2147483647; 139 | files = ( 140 | ); 141 | runOnlyForDeploymentPostprocessing = 0; 142 | }; 143 | /* End PBXFrameworksBuildPhase section */ 144 | 145 | /* Begin PBXGroup section */ 146 | 1004FECF2579578E008A6797 /* Localization */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | 1004FED0257957AD008A6797 /* Localization.swift */, 150 | 1004FED6257957D1008A6797 /* Localization+Cards.swift */, 151 | 102A4648258BD05E00301BC7 /* Localization+Favorites.swift */, 152 | ); 153 | path = Localization; 154 | sourceTree = ""; 155 | }; 156 | 1004FEE52579593D008A6797 /* Localization */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 1004FEE825795A5C008A6797 /* Localizable.strings */, 160 | ); 161 | path = Localization; 162 | sourceTree = ""; 163 | }; 164 | 1004FEEE25795ED7008A6797 /* Representables */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | 1004FEEF25795EE6008A6797 /* ActivityIndicator.swift */, 168 | ); 169 | path = Representables; 170 | sourceTree = ""; 171 | }; 172 | 101D632A257A94940082D944 /* Buttons */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | 101D632C257A94BE0082D944 /* FavoriteButton.swift */, 176 | ); 177 | path = Buttons; 178 | sourceTree = ""; 179 | }; 180 | 101D632B257A949F0082D944 /* Items */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 10D3D0872577B6270025C730 /* CardItemView.swift */, 184 | ); 185 | path = Items; 186 | sourceTree = ""; 187 | }; 188 | 10976A2F258BC68000CC4355 /* Main */ = { 189 | isa = PBXGroup; 190 | children = ( 191 | 10976A35258BC69F00CC4355 /* MainCore.swift */, 192 | 10976A30258BC68F00CC4355 /* MainView.swift */, 193 | ); 194 | path = Main; 195 | sourceTree = ""; 196 | }; 197 | 10976A40258BC97C00CC4355 /* Favorites */ = { 198 | isa = PBXGroup; 199 | children = ( 200 | 10976A44258BCB0300CC4355 /* FavoritesCore.swift */, 201 | 10976A45258BCB0300CC4355 /* FavoritesView.swift */, 202 | ); 203 | path = Favorites; 204 | sourceTree = ""; 205 | }; 206 | 10B2D6A82583812800A9C65D /* CoreData */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | 10B2D6A9258381BC00A9C65D /* PokemonCards.xcdatamodeld */, 210 | 10B2D6B625838CD200A9C65D /* CoreData.swift */, 211 | 10976A24258BAC9F00CC4355 /* FavoriteCard+Utility.swift */, 212 | ); 213 | path = CoreData; 214 | sourceTree = ""; 215 | }; 216 | 10CD45AB23C4E58600E0D0D4 = { 217 | isa = PBXGroup; 218 | children = ( 219 | 10CD45B623C4E58600E0D0D4 /* PokemonCards */, 220 | 10E404A625706D6900B1FA1C /* PokemonCardsTests */, 221 | 10D63474258A72A3009CB28D /* PokemonCardsSnapshotTests */, 222 | 10CD45B523C4E58600E0D0D4 /* Products */, 223 | 10DFBEF7258A55200078C982 /* Frameworks */, 224 | ); 225 | sourceTree = ""; 226 | }; 227 | 10CD45B523C4E58600E0D0D4 /* Products */ = { 228 | isa = PBXGroup; 229 | children = ( 230 | 10CD45B423C4E58600E0D0D4 /* PokemonCards.app */, 231 | 10E404A525706D6900B1FA1C /* PokemonCardsTests.xctest */, 232 | 10D63473258A72A3009CB28D /* PokemonCardsSnapshotTests.xctest */, 233 | ); 234 | name = Products; 235 | sourceTree = ""; 236 | }; 237 | 10CD45B623C4E58600E0D0D4 /* PokemonCards */ = { 238 | isa = PBXGroup; 239 | children = ( 240 | 1004FECF2579578E008A6797 /* Localization */, 241 | 10F6F4F425710773001851A5 /* Utility */, 242 | 10E37998256FFDBD00B2CFBE /* Data */, 243 | 10D3D0852577B5DA0025C730 /* Design System */, 244 | 10E37999256FFDF600B2CFBE /* Application */, 245 | 10CD45E823C4EA5C00E0D0D4 /* Resources */, 246 | ); 247 | path = PokemonCards; 248 | sourceTree = ""; 249 | }; 250 | 10CD45BF23C4E58700E0D0D4 /* Preview Content */ = { 251 | isa = PBXGroup; 252 | children = ( 253 | 10CD45C023C4E58700E0D0D4 /* Preview Assets.xcassets */, 254 | ); 255 | path = "Preview Content"; 256 | sourceTree = ""; 257 | }; 258 | 10CD45E823C4EA5C00E0D0D4 /* Resources */ = { 259 | isa = PBXGroup; 260 | children = ( 261 | 10E379B4257003F400B2CFBE /* Configs */, 262 | 1004FEE52579593D008A6797 /* Localization */, 263 | 10CD45BF23C4E58700E0D0D4 /* Preview Content */, 264 | 10CD45BD23C4E58700E0D0D4 /* Assets.xcassets */, 265 | 10CD45C523C4E58700E0D0D4 /* Info.plist */, 266 | ); 267 | path = Resources; 268 | sourceTree = ""; 269 | }; 270 | 10CD45E923C4EA9700E0D0D4 /* Launch */ = { 271 | isa = PBXGroup; 272 | children = ( 273 | 10CD45B923C4E58600E0D0D4 /* SceneDelegate.swift */, 274 | 10CD45B723C4E58600E0D0D4 /* AppDelegate.swift */, 275 | 10CD45C223C4E58700E0D0D4 /* LaunchScreen.storyboard */, 276 | ); 277 | path = Launch; 278 | sourceTree = ""; 279 | }; 280 | 10D3D0802577B0CA0025C730 /* CardDetail */ = { 281 | isa = PBXGroup; 282 | children = ( 283 | 10D3D0812577B0DF0025C730 /* CardDetailCore.swift */, 284 | 1004FEC925794FD7008A6797 /* CardDetailView.swift */, 285 | ); 286 | path = CardDetail; 287 | sourceTree = ""; 288 | }; 289 | 10D3D0852577B5DA0025C730 /* Design System */ = { 290 | isa = PBXGroup; 291 | children = ( 292 | 1004FEEE25795ED7008A6797 /* Representables */, 293 | 10D3D0862577B5F60025C730 /* Components */, 294 | ); 295 | path = "Design System"; 296 | sourceTree = ""; 297 | }; 298 | 10D3D0862577B5F60025C730 /* Components */ = { 299 | isa = PBXGroup; 300 | children = ( 301 | 101D632B257A949F0082D944 /* Items */, 302 | 101D632A257A94940082D944 /* Buttons */, 303 | ); 304 | path = Components; 305 | sourceTree = ""; 306 | }; 307 | 10D63474258A72A3009CB28D /* PokemonCardsSnapshotTests */ = { 308 | isa = PBXGroup; 309 | children = ( 310 | 10D63495258A72EF009CB28D /* PokemonCardsSnapshotTests.swift */, 311 | 10D63477258A72A3009CB28D /* Info.plist */, 312 | ); 313 | path = PokemonCardsSnapshotTests; 314 | sourceTree = ""; 315 | }; 316 | 10DFBEF7258A55200078C982 /* Frameworks */ = { 317 | isa = PBXGroup; 318 | children = ( 319 | ); 320 | name = Frameworks; 321 | sourceTree = ""; 322 | }; 323 | 10E37998256FFDBD00B2CFBE /* Data */ = { 324 | isa = PBXGroup; 325 | children = ( 326 | 10F6F514257125C7001851A5 /* Client */, 327 | 10E3799D256FFE1F00B2CFBE /* Model */, 328 | ); 329 | path = Data; 330 | sourceTree = ""; 331 | }; 332 | 10E37999256FFDF600B2CFBE /* Application */ = { 333 | isa = PBXGroup; 334 | children = ( 335 | 10CD45E923C4EA9700E0D0D4 /* Launch */, 336 | 10976A2F258BC68000CC4355 /* Main */, 337 | 10E379FA25701A5C00B2CFBE /* Cards */, 338 | 10976A40258BC97C00CC4355 /* Favorites */, 339 | 10D3D0802577B0CA0025C730 /* CardDetail */, 340 | ); 341 | path = Application; 342 | sourceTree = ""; 343 | }; 344 | 10E3799D256FFE1F00B2CFBE /* Model */ = { 345 | isa = PBXGroup; 346 | children = ( 347 | 10D3D068257799120025C730 /* Cards.swift */, 348 | ); 349 | path = Model; 350 | sourceTree = ""; 351 | }; 352 | 10E3799E256FFE2600B2CFBE /* Provider */ = { 353 | isa = PBXGroup; 354 | children = ( 355 | 10E379A0256FFE5800B2CFBE /* Provider.swift */, 356 | 10E379EB25700CCB00B2CFBE /* Provider+Cards.swift */, 357 | 10B2D6AE258384AB00A9C65D /* Provider+FavoriteCards.swift */, 358 | 10F6F4FB257108B2001851A5 /* ProviderError.swift */, 359 | 10E3799F256FFE3100B2CFBE /* Router */, 360 | 10B2D6A82583812800A9C65D /* CoreData */, 361 | ); 362 | path = Provider; 363 | sourceTree = ""; 364 | }; 365 | 10E3799F256FFE3100B2CFBE /* Router */ = { 366 | isa = PBXGroup; 367 | children = ( 368 | 10E379AE2570034100B2CFBE /* Router.swift */, 369 | 10E379E625700AEC00B2CFBE /* Router+Cards.swift */, 370 | ); 371 | path = Router; 372 | sourceTree = ""; 373 | }; 374 | 10E379B4257003F400B2CFBE /* Configs */ = { 375 | isa = PBXGroup; 376 | children = ( 377 | 10E379B62570049A00B2CFBE /* Staging.xcconfig */, 378 | 10E379B52570040B00B2CFBE /* Production.xcconfig */, 379 | ); 380 | path = Configs; 381 | sourceTree = ""; 382 | }; 383 | 10E379FA25701A5C00B2CFBE /* Cards */ = { 384 | isa = PBXGroup; 385 | children = ( 386 | 10E37A0125701B2B00B2CFBE /* CardsCore.swift */, 387 | 10E379F525701A5700B2CFBE /* CardsView.swift */, 388 | ); 389 | path = Cards; 390 | sourceTree = ""; 391 | }; 392 | 10E404A625706D6900B1FA1C /* PokemonCardsTests */ = { 393 | isa = PBXGroup; 394 | children = ( 395 | 10E404B225706D7C00B1FA1C /* PokemonCardsTests.swift */, 396 | 10E404B325706D7C00B1FA1C /* Info.plist */, 397 | ); 398 | path = PokemonCardsTests; 399 | sourceTree = ""; 400 | }; 401 | 10F6F4F425710773001851A5 /* Utility */ = { 402 | isa = PBXGroup; 403 | children = ( 404 | 10E379E1257007A200B2CFBE /* Environment.swift */, 405 | 10F6F4F525710784001851A5 /* Error+Equality.swift */, 406 | 108C413D2583D5C100615793 /* UUID+Increment.swift */, 407 | ); 408 | path = Utility; 409 | sourceTree = ""; 410 | }; 411 | 10F6F514257125C7001851A5 /* Client */ = { 412 | isa = PBXGroup; 413 | children = ( 414 | 10F6F515257125EF001851A5 /* CardsClient.swift */, 415 | 108C41332583BA5D00615793 /* FavoriteCardsClient.swift */, 416 | 10E3799E256FFE2600B2CFBE /* Provider */, 417 | ); 418 | path = Client; 419 | sourceTree = ""; 420 | }; 421 | /* End PBXGroup section */ 422 | 423 | /* Begin PBXNativeTarget section */ 424 | 10CD45B323C4E58600E0D0D4 /* PokemonCards */ = { 425 | isa = PBXNativeTarget; 426 | buildConfigurationList = 10CD45DE23C4E58700E0D0D4 /* Build configuration list for PBXNativeTarget "PokemonCards" */; 427 | buildPhases = ( 428 | 10CD45EE23C4F86B00E0D0D4 /* SwiftLint */, 429 | 10CD45B023C4E58600E0D0D4 /* Sources */, 430 | 10CD45B123C4E58600E0D0D4 /* Frameworks */, 431 | 10CD45B223C4E58600E0D0D4 /* Resources */, 432 | ); 433 | buildRules = ( 434 | ); 435 | dependencies = ( 436 | ); 437 | name = PokemonCards; 438 | packageProductDependencies = ( 439 | 10E4047225706B0900B1FA1C /* ComposableArchitecture */, 440 | 10D3D0912577B9AC0025C730 /* KingfisherSwiftUI */, 441 | ); 442 | productName = PokemonCards; 443 | productReference = 10CD45B423C4E58600E0D0D4 /* PokemonCards.app */; 444 | productType = "com.apple.product-type.application"; 445 | }; 446 | 10D63472258A72A3009CB28D /* PokemonCardsSnapshotTests */ = { 447 | isa = PBXNativeTarget; 448 | buildConfigurationList = 10D6347E258A72A4009CB28D /* Build configuration list for PBXNativeTarget "PokemonCardsSnapshotTests" */; 449 | buildPhases = ( 450 | 10D6346F258A72A3009CB28D /* Sources */, 451 | 10D63470258A72A3009CB28D /* Frameworks */, 452 | 10D63471258A72A3009CB28D /* Resources */, 453 | ); 454 | buildRules = ( 455 | ); 456 | dependencies = ( 457 | 10D63479258A72A3009CB28D /* PBXTargetDependency */, 458 | 10D6349B258A7317009CB28D /* PBXTargetDependency */, 459 | ); 460 | name = PokemonCardsSnapshotTests; 461 | packageProductDependencies = ( 462 | 10D6349C258A7321009CB28D /* SnapshotTesting */, 463 | ); 464 | productName = PokemonCardsSnapshotTests; 465 | productReference = 10D63473258A72A3009CB28D /* PokemonCardsSnapshotTests.xctest */; 466 | productType = "com.apple.product-type.bundle.unit-test"; 467 | }; 468 | 10E404A425706D6900B1FA1C /* PokemonCardsTests */ = { 469 | isa = PBXNativeTarget; 470 | buildConfigurationList = 10E404AC25706D6900B1FA1C /* Build configuration list for PBXNativeTarget "PokemonCardsTests" */; 471 | buildPhases = ( 472 | 10E404A125706D6900B1FA1C /* Sources */, 473 | 10E404A225706D6900B1FA1C /* Frameworks */, 474 | 10E404A325706D6900B1FA1C /* Resources */, 475 | ); 476 | buildRules = ( 477 | ); 478 | dependencies = ( 479 | 10E404AB25706D6900B1FA1C /* PBXTargetDependency */, 480 | ); 481 | name = PokemonCardsTests; 482 | packageProductDependencies = ( 483 | ); 484 | productName = PokemonCardsTests; 485 | productReference = 10E404A525706D6900B1FA1C /* PokemonCardsTests.xctest */; 486 | productType = "com.apple.product-type.bundle.unit-test"; 487 | }; 488 | /* End PBXNativeTarget section */ 489 | 490 | /* Begin PBXProject section */ 491 | 10CD45AC23C4E58600E0D0D4 /* Project object */ = { 492 | isa = PBXProject; 493 | attributes = { 494 | LastSwiftUpdateCheck = 1220; 495 | LastUpgradeCheck = 1200; 496 | ORGANIZATIONNAME = Coletiv; 497 | TargetAttributes = { 498 | 10CD45B323C4E58600E0D0D4 = { 499 | CreatedOnToolsVersion = 11.2.1; 500 | }; 501 | 10D63472258A72A3009CB28D = { 502 | CreatedOnToolsVersion = 12.2; 503 | LastSwiftMigration = 1220; 504 | TestTargetID = 10CD45B323C4E58600E0D0D4; 505 | }; 506 | 10E404A425706D6900B1FA1C = { 507 | CreatedOnToolsVersion = 12.2; 508 | TestTargetID = 10CD45B323C4E58600E0D0D4; 509 | }; 510 | }; 511 | }; 512 | buildConfigurationList = 10CD45AF23C4E58600E0D0D4 /* Build configuration list for PBXProject "PokemonCards" */; 513 | compatibilityVersion = "Xcode 9.3"; 514 | developmentRegion = en; 515 | hasScannedForEncodings = 0; 516 | knownRegions = ( 517 | en, 518 | Base, 519 | "pt-PT", 520 | ); 521 | mainGroup = 10CD45AB23C4E58600E0D0D4; 522 | packageReferences = ( 523 | 10E4047125706B0900B1FA1C /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, 524 | 10D3D0902577B9AC0025C730 /* XCRemoteSwiftPackageReference "Kingfisher" */, 525 | 10DFBEE8258A106F0078C982 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, 526 | ); 527 | productRefGroup = 10CD45B523C4E58600E0D0D4 /* Products */; 528 | projectDirPath = ""; 529 | projectRoot = ""; 530 | targets = ( 531 | 10CD45B323C4E58600E0D0D4 /* PokemonCards */, 532 | 10E404A425706D6900B1FA1C /* PokemonCardsTests */, 533 | 10D63472258A72A3009CB28D /* PokemonCardsSnapshotTests */, 534 | ); 535 | }; 536 | /* End PBXProject section */ 537 | 538 | /* Begin PBXResourcesBuildPhase section */ 539 | 10CD45B223C4E58600E0D0D4 /* Resources */ = { 540 | isa = PBXResourcesBuildPhase; 541 | buildActionMask = 2147483647; 542 | files = ( 543 | 10CD45C423C4E58700E0D0D4 /* LaunchScreen.storyboard in Resources */, 544 | 1004FEE625795A5C008A6797 /* Localizable.strings in Resources */, 545 | 10CD45C123C4E58700E0D0D4 /* Preview Assets.xcassets in Resources */, 546 | 10CD45BE23C4E58700E0D0D4 /* Assets.xcassets in Resources */, 547 | ); 548 | runOnlyForDeploymentPostprocessing = 0; 549 | }; 550 | 10D63471258A72A3009CB28D /* Resources */ = { 551 | isa = PBXResourcesBuildPhase; 552 | buildActionMask = 2147483647; 553 | files = ( 554 | ); 555 | runOnlyForDeploymentPostprocessing = 0; 556 | }; 557 | 10E404A325706D6900B1FA1C /* Resources */ = { 558 | isa = PBXResourcesBuildPhase; 559 | buildActionMask = 2147483647; 560 | files = ( 561 | ); 562 | runOnlyForDeploymentPostprocessing = 0; 563 | }; 564 | /* End PBXResourcesBuildPhase section */ 565 | 566 | /* Begin PBXShellScriptBuildPhase section */ 567 | 10CD45EE23C4F86B00E0D0D4 /* SwiftLint */ = { 568 | isa = PBXShellScriptBuildPhase; 569 | buildActionMask = 2147483647; 570 | files = ( 571 | ); 572 | inputFileListPaths = ( 573 | ); 574 | inputPaths = ( 575 | ); 576 | name = SwiftLint; 577 | outputFileListPaths = ( 578 | ); 579 | outputPaths = ( 580 | ); 581 | runOnlyForDeploymentPostprocessing = 0; 582 | shellPath = /bin/sh; 583 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, run: brew install swiftlint\"\nfi\n"; 584 | }; 585 | /* End PBXShellScriptBuildPhase section */ 586 | 587 | /* Begin PBXSourcesBuildPhase section */ 588 | 10CD45B023C4E58600E0D0D4 /* Sources */ = { 589 | isa = PBXSourcesBuildPhase; 590 | buildActionMask = 2147483647; 591 | files = ( 592 | 108C41342583BA5D00615793 /* FavoriteCardsClient.swift in Sources */, 593 | 10E379A1256FFE5800B2CFBE /* Provider.swift in Sources */, 594 | 1004FED1257957AD008A6797 /* Localization.swift in Sources */, 595 | 1004FEF025795EE6008A6797 /* ActivityIndicator.swift in Sources */, 596 | 10E379F625701A5700B2CFBE /* CardsView.swift in Sources */, 597 | 10D3D069257799120025C730 /* Cards.swift in Sources */, 598 | 102A4641258BCD0300301BC7 /* FavoritesView.swift in Sources */, 599 | 1004FED7257957D1008A6797 /* Localization+Cards.swift in Sources */, 600 | 10E379E2257007A200B2CFBE /* Environment.swift in Sources */, 601 | 10F6F516257125EF001851A5 /* CardsClient.swift in Sources */, 602 | 10D3D0882577B6270025C730 /* CardItemView.swift in Sources */, 603 | 10E379EC25700CCB00B2CFBE /* Provider+Cards.swift in Sources */, 604 | 1004FECA25794FD7008A6797 /* CardDetailView.swift in Sources */, 605 | 10CD45B823C4E58600E0D0D4 /* AppDelegate.swift in Sources */, 606 | 10F6F4F625710784001851A5 /* Error+Equality.swift in Sources */, 607 | 10E37A0225701B2B00B2CFBE /* CardsCore.swift in Sources */, 608 | 10976A36258BC69F00CC4355 /* MainCore.swift in Sources */, 609 | 10B2D6AF258384AB00A9C65D /* Provider+FavoriteCards.swift in Sources */, 610 | 10976A25258BAC9F00CC4355 /* FavoriteCard+Utility.swift in Sources */, 611 | 10F6F4FC257108B2001851A5 /* ProviderError.swift in Sources */, 612 | 10E379AF2570034100B2CFBE /* Router.swift in Sources */, 613 | 108C413E2583D5C100615793 /* UUID+Increment.swift in Sources */, 614 | 10CD45BA23C4E58600E0D0D4 /* SceneDelegate.swift in Sources */, 615 | 10E379E725700AEC00B2CFBE /* Router+Cards.swift in Sources */, 616 | 10B2D6AB258381BC00A9C65D /* PokemonCards.xcdatamodeld in Sources */, 617 | 10976A31258BC68F00CC4355 /* MainView.swift in Sources */, 618 | 102A463D258BCC8D00301BC7 /* FavoritesCore.swift in Sources */, 619 | 10D3D0822577B0DF0025C730 /* CardDetailCore.swift in Sources */, 620 | 10B2D6B725838CD200A9C65D /* CoreData.swift in Sources */, 621 | 102A4649258BD05E00301BC7 /* Localization+Favorites.swift in Sources */, 622 | 101D632D257A94BE0082D944 /* FavoriteButton.swift in Sources */, 623 | ); 624 | runOnlyForDeploymentPostprocessing = 0; 625 | }; 626 | 10D6346F258A72A3009CB28D /* Sources */ = { 627 | isa = PBXSourcesBuildPhase; 628 | buildActionMask = 2147483647; 629 | files = ( 630 | 10D63496258A72EF009CB28D /* PokemonCardsSnapshotTests.swift in Sources */, 631 | ); 632 | runOnlyForDeploymentPostprocessing = 0; 633 | }; 634 | 10E404A125706D6900B1FA1C /* Sources */ = { 635 | isa = PBXSourcesBuildPhase; 636 | buildActionMask = 2147483647; 637 | files = ( 638 | 10E404B425706D7C00B1FA1C /* PokemonCardsTests.swift in Sources */, 639 | ); 640 | runOnlyForDeploymentPostprocessing = 0; 641 | }; 642 | /* End PBXSourcesBuildPhase section */ 643 | 644 | /* Begin PBXTargetDependency section */ 645 | 10D63479258A72A3009CB28D /* PBXTargetDependency */ = { 646 | isa = PBXTargetDependency; 647 | target = 10CD45B323C4E58600E0D0D4 /* PokemonCards */; 648 | targetProxy = 10D63478258A72A3009CB28D /* PBXContainerItemProxy */; 649 | }; 650 | 10D6349B258A7317009CB28D /* PBXTargetDependency */ = { 651 | isa = PBXTargetDependency; 652 | productRef = 10D6349A258A7317009CB28D /* SnapshotTesting */; 653 | }; 654 | 10E404AB25706D6900B1FA1C /* PBXTargetDependency */ = { 655 | isa = PBXTargetDependency; 656 | target = 10CD45B323C4E58600E0D0D4 /* PokemonCards */; 657 | targetProxy = 10E404AA25706D6900B1FA1C /* PBXContainerItemProxy */; 658 | }; 659 | /* End PBXTargetDependency section */ 660 | 661 | /* Begin PBXVariantGroup section */ 662 | 1004FEE825795A5C008A6797 /* Localizable.strings */ = { 663 | isa = PBXVariantGroup; 664 | children = ( 665 | 1004FEE725795A5C008A6797 /* en */, 666 | 1004FEEB25795A61008A6797 /* pt-PT */, 667 | ); 668 | name = Localizable.strings; 669 | sourceTree = ""; 670 | }; 671 | 10CD45C223C4E58700E0D0D4 /* LaunchScreen.storyboard */ = { 672 | isa = PBXVariantGroup; 673 | children = ( 674 | 10CD45C323C4E58700E0D0D4 /* Base */, 675 | ); 676 | name = LaunchScreen.storyboard; 677 | sourceTree = ""; 678 | }; 679 | /* End PBXVariantGroup section */ 680 | 681 | /* Begin XCBuildConfiguration section */ 682 | 10CD45DC23C4E58700E0D0D4 /* Debug-Staging */ = { 683 | isa = XCBuildConfiguration; 684 | baseConfigurationReference = 10E379B62570049A00B2CFBE /* Staging.xcconfig */; 685 | buildSettings = { 686 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 687 | ALWAYS_SEARCH_USER_PATHS = NO; 688 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 689 | CLANG_ANALYZER_NONNULL = YES; 690 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 691 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 692 | CLANG_CXX_LIBRARY = "libc++"; 693 | CLANG_ENABLE_MODULES = YES; 694 | CLANG_ENABLE_OBJC_ARC = YES; 695 | CLANG_ENABLE_OBJC_WEAK = YES; 696 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 697 | CLANG_WARN_BOOL_CONVERSION = YES; 698 | CLANG_WARN_COMMA = YES; 699 | CLANG_WARN_CONSTANT_CONVERSION = YES; 700 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 701 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 702 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 703 | CLANG_WARN_EMPTY_BODY = YES; 704 | CLANG_WARN_ENUM_CONVERSION = YES; 705 | CLANG_WARN_INFINITE_RECURSION = YES; 706 | CLANG_WARN_INT_CONVERSION = YES; 707 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 708 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 709 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 710 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 711 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 712 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 713 | CLANG_WARN_STRICT_PROTOTYPES = YES; 714 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 715 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 716 | CLANG_WARN_UNREACHABLE_CODE = YES; 717 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 718 | COPY_PHASE_STRIP = NO; 719 | DEBUG_INFORMATION_FORMAT = dwarf; 720 | ENABLE_STRICT_OBJC_MSGSEND = YES; 721 | ENABLE_TESTABILITY = YES; 722 | GCC_C_LANGUAGE_STANDARD = gnu11; 723 | GCC_DYNAMIC_NO_PIC = NO; 724 | GCC_NO_COMMON_BLOCKS = YES; 725 | GCC_OPTIMIZATION_LEVEL = 0; 726 | GCC_PREPROCESSOR_DEFINITIONS = ( 727 | "DEBUG=1", 728 | "$(inherited)", 729 | ); 730 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 731 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 732 | GCC_WARN_UNDECLARED_SELECTOR = YES; 733 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 734 | GCC_WARN_UNUSED_FUNCTION = YES; 735 | GCC_WARN_UNUSED_VARIABLE = YES; 736 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 737 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 738 | MTL_FAST_MATH = YES; 739 | ONLY_ACTIVE_ARCH = YES; 740 | SDKROOT = iphoneos; 741 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 742 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 743 | }; 744 | name = "Debug-Staging"; 745 | }; 746 | 10CD45DD23C4E58700E0D0D4 /* Release-Production */ = { 747 | isa = XCBuildConfiguration; 748 | baseConfigurationReference = 10E379B52570040B00B2CFBE /* Production.xcconfig */; 749 | buildSettings = { 750 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 751 | ALWAYS_SEARCH_USER_PATHS = NO; 752 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 753 | CLANG_ANALYZER_NONNULL = YES; 754 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 755 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 756 | CLANG_CXX_LIBRARY = "libc++"; 757 | CLANG_ENABLE_MODULES = YES; 758 | CLANG_ENABLE_OBJC_ARC = YES; 759 | CLANG_ENABLE_OBJC_WEAK = YES; 760 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 761 | CLANG_WARN_BOOL_CONVERSION = YES; 762 | CLANG_WARN_COMMA = YES; 763 | CLANG_WARN_CONSTANT_CONVERSION = YES; 764 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 765 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 766 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 767 | CLANG_WARN_EMPTY_BODY = YES; 768 | CLANG_WARN_ENUM_CONVERSION = YES; 769 | CLANG_WARN_INFINITE_RECURSION = YES; 770 | CLANG_WARN_INT_CONVERSION = YES; 771 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 772 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 773 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 774 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 775 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 776 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 777 | CLANG_WARN_STRICT_PROTOTYPES = YES; 778 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 779 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 780 | CLANG_WARN_UNREACHABLE_CODE = YES; 781 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 782 | COPY_PHASE_STRIP = NO; 783 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 784 | ENABLE_NS_ASSERTIONS = NO; 785 | ENABLE_STRICT_OBJC_MSGSEND = YES; 786 | GCC_C_LANGUAGE_STANDARD = gnu11; 787 | GCC_NO_COMMON_BLOCKS = YES; 788 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 789 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 790 | GCC_WARN_UNDECLARED_SELECTOR = YES; 791 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 792 | GCC_WARN_UNUSED_FUNCTION = YES; 793 | GCC_WARN_UNUSED_VARIABLE = YES; 794 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 795 | MTL_ENABLE_DEBUG_INFO = NO; 796 | MTL_FAST_MATH = YES; 797 | SDKROOT = iphoneos; 798 | SWIFT_COMPILATION_MODE = wholemodule; 799 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 800 | VALIDATE_PRODUCT = YES; 801 | }; 802 | name = "Release-Production"; 803 | }; 804 | 10CD45DF23C4E58700E0D0D4 /* Debug-Staging */ = { 805 | isa = XCBuildConfiguration; 806 | baseConfigurationReference = 10E379B62570049A00B2CFBE /* Staging.xcconfig */; 807 | buildSettings = { 808 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 809 | CODE_SIGN_IDENTITY = "Apple Development"; 810 | CODE_SIGN_STYLE = Automatic; 811 | DEVELOPMENT_ASSET_PATHS = "PokemonCards/Resources/Preview\\ Content/Preview\\ Assets.xcassets"; 812 | DEVELOPMENT_TEAM = RGK82JLN9X; 813 | ENABLE_PREVIEWS = YES; 814 | INFOPLIST_FILE = PokemonCards/Resources/Info.plist; 815 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 816 | LD_RUNPATH_SEARCH_PATHS = ( 817 | "$(inherited)", 818 | "@executable_path/Frameworks", 819 | ); 820 | MARKETING_VERSION = 1.0.0; 821 | OTHER_SWIFT_FLAGS = ""; 822 | PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_ID)"; 823 | PRODUCT_NAME = "$(TARGET_NAME)"; 824 | PROVISIONING_PROFILE_SPECIFIER = ""; 825 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 826 | SWIFT_VERSION = 5.0; 827 | TARGETED_DEVICE_FAMILY = "1,2"; 828 | }; 829 | name = "Debug-Staging"; 830 | }; 831 | 10CD45E023C4E58700E0D0D4 /* Release-Production */ = { 832 | isa = XCBuildConfiguration; 833 | buildSettings = { 834 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 835 | CODE_SIGN_IDENTITY = "Apple Development"; 836 | CODE_SIGN_STYLE = Automatic; 837 | DEVELOPMENT_ASSET_PATHS = "PokemonCards/Resources/Preview\\ Content/Preview\\ Assets.xcassets"; 838 | DEVELOPMENT_TEAM = RGK82JLN9X; 839 | ENABLE_PREVIEWS = YES; 840 | INFOPLIST_FILE = PokemonCards/Resources/Info.plist; 841 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 842 | LD_RUNPATH_SEARCH_PATHS = ( 843 | "$(inherited)", 844 | "@executable_path/Frameworks", 845 | ); 846 | MARKETING_VERSION = 1.0.0; 847 | ONLY_ACTIVE_ARCH = YES; 848 | PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_ID)"; 849 | PRODUCT_NAME = "$(TARGET_NAME)"; 850 | PROVISIONING_PROFILE_SPECIFIER = ""; 851 | SWIFT_VERSION = 5.0; 852 | TARGETED_DEVICE_FAMILY = "1,2"; 853 | }; 854 | name = "Release-Production"; 855 | }; 856 | 10D3C1372570FF290031735E /* Debug-Production */ = { 857 | isa = XCBuildConfiguration; 858 | baseConfigurationReference = 10E379B52570040B00B2CFBE /* Production.xcconfig */; 859 | buildSettings = { 860 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 861 | ALWAYS_SEARCH_USER_PATHS = NO; 862 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 863 | CLANG_ANALYZER_NONNULL = YES; 864 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 865 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 866 | CLANG_CXX_LIBRARY = "libc++"; 867 | CLANG_ENABLE_MODULES = YES; 868 | CLANG_ENABLE_OBJC_ARC = YES; 869 | CLANG_ENABLE_OBJC_WEAK = YES; 870 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 871 | CLANG_WARN_BOOL_CONVERSION = YES; 872 | CLANG_WARN_COMMA = YES; 873 | CLANG_WARN_CONSTANT_CONVERSION = YES; 874 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 875 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 876 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 877 | CLANG_WARN_EMPTY_BODY = YES; 878 | CLANG_WARN_ENUM_CONVERSION = YES; 879 | CLANG_WARN_INFINITE_RECURSION = YES; 880 | CLANG_WARN_INT_CONVERSION = YES; 881 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 882 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 883 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 884 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 885 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 886 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 887 | CLANG_WARN_STRICT_PROTOTYPES = YES; 888 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 889 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 890 | CLANG_WARN_UNREACHABLE_CODE = YES; 891 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 892 | COPY_PHASE_STRIP = NO; 893 | DEBUG_INFORMATION_FORMAT = dwarf; 894 | ENABLE_STRICT_OBJC_MSGSEND = YES; 895 | ENABLE_TESTABILITY = YES; 896 | GCC_C_LANGUAGE_STANDARD = gnu11; 897 | GCC_DYNAMIC_NO_PIC = NO; 898 | GCC_NO_COMMON_BLOCKS = YES; 899 | GCC_OPTIMIZATION_LEVEL = 0; 900 | GCC_PREPROCESSOR_DEFINITIONS = ( 901 | "DEBUG=1", 902 | "$(inherited)", 903 | ); 904 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 905 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 906 | GCC_WARN_UNDECLARED_SELECTOR = YES; 907 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 908 | GCC_WARN_UNUSED_FUNCTION = YES; 909 | GCC_WARN_UNUSED_VARIABLE = YES; 910 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 911 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 912 | MTL_FAST_MATH = YES; 913 | ONLY_ACTIVE_ARCH = YES; 914 | SDKROOT = iphoneos; 915 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 916 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 917 | }; 918 | name = "Debug-Production"; 919 | }; 920 | 10D3C1382570FF290031735E /* Debug-Production */ = { 921 | isa = XCBuildConfiguration; 922 | buildSettings = { 923 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 924 | CODE_SIGN_IDENTITY = "Apple Development"; 925 | CODE_SIGN_STYLE = Automatic; 926 | DEVELOPMENT_ASSET_PATHS = "PokemonCards/Resources/Preview\\ Content/Preview\\ Assets.xcassets"; 927 | DEVELOPMENT_TEAM = RGK82JLN9X; 928 | ENABLE_PREVIEWS = YES; 929 | INFOPLIST_FILE = PokemonCards/Resources/Info.plist; 930 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 931 | LD_RUNPATH_SEARCH_PATHS = ( 932 | "$(inherited)", 933 | "@executable_path/Frameworks", 934 | ); 935 | MARKETING_VERSION = 1.0.0; 936 | OTHER_SWIFT_FLAGS = ""; 937 | PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_ID)"; 938 | PRODUCT_NAME = "$(TARGET_NAME)"; 939 | PROVISIONING_PROFILE_SPECIFIER = ""; 940 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 941 | SWIFT_VERSION = 5.0; 942 | TARGETED_DEVICE_FAMILY = "1,2"; 943 | }; 944 | name = "Debug-Production"; 945 | }; 946 | 10D3C1392570FF290031735E /* Debug-Production */ = { 947 | isa = XCBuildConfiguration; 948 | buildSettings = { 949 | ALWAYS_SEARCH_USER_PATHS = NO; 950 | BUNDLE_LOADER = "$(TEST_HOST)"; 951 | CLANG_ANALYZER_NONNULL = YES; 952 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 953 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 954 | CLANG_CXX_LIBRARY = "libc++"; 955 | CLANG_ENABLE_MODULES = YES; 956 | CLANG_ENABLE_OBJC_ARC = YES; 957 | CLANG_ENABLE_OBJC_WEAK = YES; 958 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 959 | CLANG_WARN_BOOL_CONVERSION = YES; 960 | CLANG_WARN_COMMA = YES; 961 | CLANG_WARN_CONSTANT_CONVERSION = YES; 962 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 963 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 964 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 965 | CLANG_WARN_EMPTY_BODY = YES; 966 | CLANG_WARN_ENUM_CONVERSION = YES; 967 | CLANG_WARN_INFINITE_RECURSION = YES; 968 | CLANG_WARN_INT_CONVERSION = YES; 969 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 970 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 971 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 972 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 973 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 974 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 975 | CLANG_WARN_STRICT_PROTOTYPES = YES; 976 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 977 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 978 | CLANG_WARN_UNREACHABLE_CODE = YES; 979 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 980 | CODE_SIGN_STYLE = Automatic; 981 | COPY_PHASE_STRIP = NO; 982 | DEBUG_INFORMATION_FORMAT = dwarf; 983 | DEVELOPMENT_TEAM = RGK82JLN9X; 984 | ENABLE_STRICT_OBJC_MSGSEND = YES; 985 | ENABLE_TESTABILITY = YES; 986 | GCC_C_LANGUAGE_STANDARD = gnu11; 987 | GCC_DYNAMIC_NO_PIC = NO; 988 | GCC_NO_COMMON_BLOCKS = YES; 989 | GCC_OPTIMIZATION_LEVEL = 0; 990 | GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; 991 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 992 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 993 | GCC_WARN_UNDECLARED_SELECTOR = YES; 994 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 995 | GCC_WARN_UNUSED_FUNCTION = YES; 996 | GCC_WARN_UNUSED_VARIABLE = YES; 997 | INFOPLIST_FILE = PokemonCardsTests/Info.plist; 998 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 999 | LD_RUNPATH_SEARCH_PATHS = ( 1000 | "$(inherited)", 1001 | "@executable_path/Frameworks", 1002 | "@loader_path/Frameworks", 1003 | ); 1004 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 1005 | MTL_FAST_MATH = YES; 1006 | ONLY_ACTIVE_ARCH = YES; 1007 | PRODUCT_BUNDLE_IDENTIFIER = Coletiv.PokemonCardsTests; 1008 | PRODUCT_NAME = "$(TARGET_NAME)"; 1009 | SDKROOT = iphoneos; 1010 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 1011 | SWIFT_COMPILATION_MODE = singlefile; 1012 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 1013 | SWIFT_VERSION = 5.0; 1014 | TARGETED_DEVICE_FAMILY = "1,2"; 1015 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PokemonCards.app/PokemonCards"; 1016 | }; 1017 | name = "Debug-Production"; 1018 | }; 1019 | 10D3C13A2570FF4B0031735E /* Release-Staging */ = { 1020 | isa = XCBuildConfiguration; 1021 | baseConfigurationReference = 10E379B62570049A00B2CFBE /* Staging.xcconfig */; 1022 | buildSettings = { 1023 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 1024 | ALWAYS_SEARCH_USER_PATHS = NO; 1025 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 1026 | CLANG_ANALYZER_NONNULL = YES; 1027 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 1028 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 1029 | CLANG_CXX_LIBRARY = "libc++"; 1030 | CLANG_ENABLE_MODULES = YES; 1031 | CLANG_ENABLE_OBJC_ARC = YES; 1032 | CLANG_ENABLE_OBJC_WEAK = YES; 1033 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 1034 | CLANG_WARN_BOOL_CONVERSION = YES; 1035 | CLANG_WARN_COMMA = YES; 1036 | CLANG_WARN_CONSTANT_CONVERSION = YES; 1037 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 1038 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 1039 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 1040 | CLANG_WARN_EMPTY_BODY = YES; 1041 | CLANG_WARN_ENUM_CONVERSION = YES; 1042 | CLANG_WARN_INFINITE_RECURSION = YES; 1043 | CLANG_WARN_INT_CONVERSION = YES; 1044 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 1045 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 1046 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 1047 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 1048 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 1049 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 1050 | CLANG_WARN_STRICT_PROTOTYPES = YES; 1051 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 1052 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 1053 | CLANG_WARN_UNREACHABLE_CODE = YES; 1054 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 1055 | COPY_PHASE_STRIP = NO; 1056 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 1057 | ENABLE_NS_ASSERTIONS = NO; 1058 | ENABLE_STRICT_OBJC_MSGSEND = YES; 1059 | GCC_C_LANGUAGE_STANDARD = gnu11; 1060 | GCC_NO_COMMON_BLOCKS = YES; 1061 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 1062 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 1063 | GCC_WARN_UNDECLARED_SELECTOR = YES; 1064 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 1065 | GCC_WARN_UNUSED_FUNCTION = YES; 1066 | GCC_WARN_UNUSED_VARIABLE = YES; 1067 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 1068 | MTL_ENABLE_DEBUG_INFO = NO; 1069 | MTL_FAST_MATH = YES; 1070 | SDKROOT = iphoneos; 1071 | SWIFT_COMPILATION_MODE = wholemodule; 1072 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 1073 | VALIDATE_PRODUCT = YES; 1074 | }; 1075 | name = "Release-Staging"; 1076 | }; 1077 | 10D3C13B2570FF4B0031735E /* Release-Staging */ = { 1078 | isa = XCBuildConfiguration; 1079 | buildSettings = { 1080 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 1081 | CODE_SIGN_IDENTITY = "Apple Development"; 1082 | CODE_SIGN_STYLE = Automatic; 1083 | DEVELOPMENT_ASSET_PATHS = "PokemonCards/Resources/Preview\\ Content/Preview\\ Assets.xcassets"; 1084 | DEVELOPMENT_TEAM = RGK82JLN9X; 1085 | ENABLE_PREVIEWS = YES; 1086 | INFOPLIST_FILE = PokemonCards/Resources/Info.plist; 1087 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 1088 | LD_RUNPATH_SEARCH_PATHS = ( 1089 | "$(inherited)", 1090 | "@executable_path/Frameworks", 1091 | ); 1092 | MARKETING_VERSION = 1.0.0; 1093 | ONLY_ACTIVE_ARCH = YES; 1094 | PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_ID)"; 1095 | PRODUCT_NAME = "$(TARGET_NAME)"; 1096 | PROVISIONING_PROFILE_SPECIFIER = ""; 1097 | SWIFT_VERSION = 5.0; 1098 | TARGETED_DEVICE_FAMILY = "1,2"; 1099 | }; 1100 | name = "Release-Staging"; 1101 | }; 1102 | 10D3C13C2570FF4B0031735E /* Release-Staging */ = { 1103 | isa = XCBuildConfiguration; 1104 | buildSettings = { 1105 | ALWAYS_SEARCH_USER_PATHS = NO; 1106 | BUNDLE_LOADER = "$(TEST_HOST)"; 1107 | CLANG_ANALYZER_NONNULL = YES; 1108 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 1109 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 1110 | CLANG_CXX_LIBRARY = "libc++"; 1111 | CLANG_ENABLE_MODULES = YES; 1112 | CLANG_ENABLE_OBJC_ARC = YES; 1113 | CLANG_ENABLE_OBJC_WEAK = YES; 1114 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 1115 | CLANG_WARN_BOOL_CONVERSION = YES; 1116 | CLANG_WARN_COMMA = YES; 1117 | CLANG_WARN_CONSTANT_CONVERSION = YES; 1118 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 1119 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 1120 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 1121 | CLANG_WARN_EMPTY_BODY = YES; 1122 | CLANG_WARN_ENUM_CONVERSION = YES; 1123 | CLANG_WARN_INFINITE_RECURSION = YES; 1124 | CLANG_WARN_INT_CONVERSION = YES; 1125 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 1126 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 1127 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 1128 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 1129 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 1130 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 1131 | CLANG_WARN_STRICT_PROTOTYPES = YES; 1132 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 1133 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 1134 | CLANG_WARN_UNREACHABLE_CODE = YES; 1135 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 1136 | CODE_SIGN_STYLE = Automatic; 1137 | COPY_PHASE_STRIP = NO; 1138 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 1139 | DEVELOPMENT_TEAM = RGK82JLN9X; 1140 | ENABLE_NS_ASSERTIONS = NO; 1141 | ENABLE_STRICT_OBJC_MSGSEND = YES; 1142 | GCC_C_LANGUAGE_STANDARD = gnu11; 1143 | GCC_NO_COMMON_BLOCKS = YES; 1144 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 1145 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 1146 | GCC_WARN_UNDECLARED_SELECTOR = YES; 1147 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 1148 | GCC_WARN_UNUSED_FUNCTION = YES; 1149 | GCC_WARN_UNUSED_VARIABLE = YES; 1150 | INFOPLIST_FILE = PokemonCardsTests/Info.plist; 1151 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 1152 | LD_RUNPATH_SEARCH_PATHS = ( 1153 | "$(inherited)", 1154 | "@executable_path/Frameworks", 1155 | "@loader_path/Frameworks", 1156 | ); 1157 | MTL_ENABLE_DEBUG_INFO = NO; 1158 | MTL_FAST_MATH = YES; 1159 | PRODUCT_BUNDLE_IDENTIFIER = Coletiv.PokemonCardsTests; 1160 | PRODUCT_NAME = "$(TARGET_NAME)"; 1161 | SDKROOT = iphoneos; 1162 | SWIFT_COMPILATION_MODE = wholemodule; 1163 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 1164 | SWIFT_VERSION = 5.0; 1165 | TARGETED_DEVICE_FAMILY = "1,2"; 1166 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PokemonCards.app/PokemonCards"; 1167 | VALIDATE_PRODUCT = YES; 1168 | }; 1169 | name = "Release-Staging"; 1170 | }; 1171 | 10D6347A258A72A3009CB28D /* Debug-Staging */ = { 1172 | isa = XCBuildConfiguration; 1173 | buildSettings = { 1174 | ALWAYS_SEARCH_USER_PATHS = NO; 1175 | BUNDLE_LOADER = "$(TEST_HOST)"; 1176 | CLANG_ANALYZER_NONNULL = YES; 1177 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 1178 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 1179 | CLANG_CXX_LIBRARY = "libc++"; 1180 | CLANG_ENABLE_MODULES = YES; 1181 | CLANG_ENABLE_OBJC_ARC = YES; 1182 | CLANG_ENABLE_OBJC_WEAK = YES; 1183 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 1184 | CLANG_WARN_BOOL_CONVERSION = YES; 1185 | CLANG_WARN_COMMA = YES; 1186 | CLANG_WARN_CONSTANT_CONVERSION = YES; 1187 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 1188 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 1189 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 1190 | CLANG_WARN_EMPTY_BODY = YES; 1191 | CLANG_WARN_ENUM_CONVERSION = YES; 1192 | CLANG_WARN_INFINITE_RECURSION = YES; 1193 | CLANG_WARN_INT_CONVERSION = YES; 1194 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 1195 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 1196 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 1197 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 1198 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 1199 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 1200 | CLANG_WARN_STRICT_PROTOTYPES = YES; 1201 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 1202 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 1203 | CLANG_WARN_UNREACHABLE_CODE = YES; 1204 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 1205 | CODE_SIGN_STYLE = Automatic; 1206 | COPY_PHASE_STRIP = NO; 1207 | DEBUG_INFORMATION_FORMAT = dwarf; 1208 | DEVELOPMENT_TEAM = RGK82JLN9X; 1209 | ENABLE_STRICT_OBJC_MSGSEND = YES; 1210 | ENABLE_TESTABILITY = YES; 1211 | GCC_C_LANGUAGE_STANDARD = gnu11; 1212 | GCC_DYNAMIC_NO_PIC = NO; 1213 | GCC_NO_COMMON_BLOCKS = YES; 1214 | GCC_OPTIMIZATION_LEVEL = 0; 1215 | GCC_PREPROCESSOR_DEFINITIONS = ( 1216 | "DEBUG=1", 1217 | "$(inherited)", 1218 | ); 1219 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 1220 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 1221 | GCC_WARN_UNDECLARED_SELECTOR = YES; 1222 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 1223 | GCC_WARN_UNUSED_FUNCTION = YES; 1224 | GCC_WARN_UNUSED_VARIABLE = YES; 1225 | INFOPLIST_FILE = PokemonCardsSnapshotTests/Info.plist; 1226 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 1227 | LD_RUNPATH_SEARCH_PATHS = ( 1228 | "$(inherited)", 1229 | "@executable_path/Frameworks", 1230 | "@loader_path/Frameworks", 1231 | ); 1232 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 1233 | MTL_FAST_MATH = YES; 1234 | ONLY_ACTIVE_ARCH = YES; 1235 | PRODUCT_BUNDLE_IDENTIFIER = Coletiv.PokemonCardsSnapshotTests; 1236 | PRODUCT_NAME = "$(TARGET_NAME)"; 1237 | SDKROOT = iphoneos; 1238 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 1239 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 1240 | SWIFT_VERSION = 5.0; 1241 | TARGETED_DEVICE_FAMILY = "1,2"; 1242 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PokemonCards.app/PokemonCards"; 1243 | }; 1244 | name = "Debug-Staging"; 1245 | }; 1246 | 10D6347B258A72A3009CB28D /* Debug-Production */ = { 1247 | isa = XCBuildConfiguration; 1248 | buildSettings = { 1249 | ALWAYS_SEARCH_USER_PATHS = NO; 1250 | BUNDLE_LOADER = "$(TEST_HOST)"; 1251 | CLANG_ANALYZER_NONNULL = YES; 1252 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 1253 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 1254 | CLANG_CXX_LIBRARY = "libc++"; 1255 | CLANG_ENABLE_MODULES = YES; 1256 | CLANG_ENABLE_OBJC_ARC = YES; 1257 | CLANG_ENABLE_OBJC_WEAK = YES; 1258 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 1259 | CLANG_WARN_BOOL_CONVERSION = YES; 1260 | CLANG_WARN_COMMA = YES; 1261 | CLANG_WARN_CONSTANT_CONVERSION = YES; 1262 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 1263 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 1264 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 1265 | CLANG_WARN_EMPTY_BODY = YES; 1266 | CLANG_WARN_ENUM_CONVERSION = YES; 1267 | CLANG_WARN_INFINITE_RECURSION = YES; 1268 | CLANG_WARN_INT_CONVERSION = YES; 1269 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 1270 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 1271 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 1272 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 1273 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 1274 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 1275 | CLANG_WARN_STRICT_PROTOTYPES = YES; 1276 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 1277 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 1278 | CLANG_WARN_UNREACHABLE_CODE = YES; 1279 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 1280 | CODE_SIGN_STYLE = Automatic; 1281 | COPY_PHASE_STRIP = NO; 1282 | DEBUG_INFORMATION_FORMAT = dwarf; 1283 | DEVELOPMENT_TEAM = RGK82JLN9X; 1284 | ENABLE_STRICT_OBJC_MSGSEND = YES; 1285 | ENABLE_TESTABILITY = YES; 1286 | GCC_C_LANGUAGE_STANDARD = gnu11; 1287 | GCC_DYNAMIC_NO_PIC = NO; 1288 | GCC_NO_COMMON_BLOCKS = YES; 1289 | GCC_OPTIMIZATION_LEVEL = 0; 1290 | GCC_PREPROCESSOR_DEFINITIONS = ( 1291 | "DEBUG=1", 1292 | "$(inherited)", 1293 | ); 1294 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 1295 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 1296 | GCC_WARN_UNDECLARED_SELECTOR = YES; 1297 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 1298 | GCC_WARN_UNUSED_FUNCTION = YES; 1299 | GCC_WARN_UNUSED_VARIABLE = YES; 1300 | INFOPLIST_FILE = PokemonCardsSnapshotTests/Info.plist; 1301 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 1302 | LD_RUNPATH_SEARCH_PATHS = ( 1303 | "$(inherited)", 1304 | "@executable_path/Frameworks", 1305 | "@loader_path/Frameworks", 1306 | ); 1307 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 1308 | MTL_FAST_MATH = YES; 1309 | ONLY_ACTIVE_ARCH = YES; 1310 | PRODUCT_BUNDLE_IDENTIFIER = Coletiv.PokemonCardsSnapshotTests; 1311 | PRODUCT_NAME = "$(TARGET_NAME)"; 1312 | SDKROOT = iphoneos; 1313 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 1314 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 1315 | SWIFT_VERSION = 5.0; 1316 | TARGETED_DEVICE_FAMILY = "1,2"; 1317 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PokemonCards.app/PokemonCards"; 1318 | }; 1319 | name = "Debug-Production"; 1320 | }; 1321 | 10D6347C258A72A3009CB28D /* Release-Production */ = { 1322 | isa = XCBuildConfiguration; 1323 | buildSettings = { 1324 | ALWAYS_SEARCH_USER_PATHS = NO; 1325 | BUNDLE_LOADER = "$(TEST_HOST)"; 1326 | CLANG_ANALYZER_NONNULL = YES; 1327 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 1328 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 1329 | CLANG_CXX_LIBRARY = "libc++"; 1330 | CLANG_ENABLE_MODULES = YES; 1331 | CLANG_ENABLE_OBJC_ARC = YES; 1332 | CLANG_ENABLE_OBJC_WEAK = YES; 1333 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 1334 | CLANG_WARN_BOOL_CONVERSION = YES; 1335 | CLANG_WARN_COMMA = YES; 1336 | CLANG_WARN_CONSTANT_CONVERSION = YES; 1337 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 1338 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 1339 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 1340 | CLANG_WARN_EMPTY_BODY = YES; 1341 | CLANG_WARN_ENUM_CONVERSION = YES; 1342 | CLANG_WARN_INFINITE_RECURSION = YES; 1343 | CLANG_WARN_INT_CONVERSION = YES; 1344 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 1345 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 1346 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 1347 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 1348 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 1349 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 1350 | CLANG_WARN_STRICT_PROTOTYPES = YES; 1351 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 1352 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 1353 | CLANG_WARN_UNREACHABLE_CODE = YES; 1354 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 1355 | CODE_SIGN_STYLE = Automatic; 1356 | COPY_PHASE_STRIP = NO; 1357 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 1358 | DEVELOPMENT_TEAM = RGK82JLN9X; 1359 | ENABLE_NS_ASSERTIONS = NO; 1360 | ENABLE_STRICT_OBJC_MSGSEND = YES; 1361 | GCC_C_LANGUAGE_STANDARD = gnu11; 1362 | GCC_NO_COMMON_BLOCKS = YES; 1363 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 1364 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 1365 | GCC_WARN_UNDECLARED_SELECTOR = YES; 1366 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 1367 | GCC_WARN_UNUSED_FUNCTION = YES; 1368 | GCC_WARN_UNUSED_VARIABLE = YES; 1369 | INFOPLIST_FILE = PokemonCardsSnapshotTests/Info.plist; 1370 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 1371 | LD_RUNPATH_SEARCH_PATHS = ( 1372 | "$(inherited)", 1373 | "@executable_path/Frameworks", 1374 | "@loader_path/Frameworks", 1375 | ); 1376 | MTL_ENABLE_DEBUG_INFO = NO; 1377 | MTL_FAST_MATH = YES; 1378 | PRODUCT_BUNDLE_IDENTIFIER = Coletiv.PokemonCardsSnapshotTests; 1379 | PRODUCT_NAME = "$(TARGET_NAME)"; 1380 | SDKROOT = iphoneos; 1381 | SWIFT_COMPILATION_MODE = wholemodule; 1382 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 1383 | SWIFT_VERSION = 5.0; 1384 | TARGETED_DEVICE_FAMILY = "1,2"; 1385 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PokemonCards.app/PokemonCards"; 1386 | VALIDATE_PRODUCT = YES; 1387 | }; 1388 | name = "Release-Production"; 1389 | }; 1390 | 10D6347D258A72A3009CB28D /* Release-Staging */ = { 1391 | isa = XCBuildConfiguration; 1392 | buildSettings = { 1393 | ALWAYS_SEARCH_USER_PATHS = NO; 1394 | BUNDLE_LOADER = "$(TEST_HOST)"; 1395 | CLANG_ANALYZER_NONNULL = YES; 1396 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 1397 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 1398 | CLANG_CXX_LIBRARY = "libc++"; 1399 | CLANG_ENABLE_MODULES = YES; 1400 | CLANG_ENABLE_OBJC_ARC = YES; 1401 | CLANG_ENABLE_OBJC_WEAK = YES; 1402 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 1403 | CLANG_WARN_BOOL_CONVERSION = YES; 1404 | CLANG_WARN_COMMA = YES; 1405 | CLANG_WARN_CONSTANT_CONVERSION = YES; 1406 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 1407 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 1408 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 1409 | CLANG_WARN_EMPTY_BODY = YES; 1410 | CLANG_WARN_ENUM_CONVERSION = YES; 1411 | CLANG_WARN_INFINITE_RECURSION = YES; 1412 | CLANG_WARN_INT_CONVERSION = YES; 1413 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 1414 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 1415 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 1416 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 1417 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 1418 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 1419 | CLANG_WARN_STRICT_PROTOTYPES = YES; 1420 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 1421 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 1422 | CLANG_WARN_UNREACHABLE_CODE = YES; 1423 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 1424 | CODE_SIGN_STYLE = Automatic; 1425 | COPY_PHASE_STRIP = NO; 1426 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 1427 | DEVELOPMENT_TEAM = RGK82JLN9X; 1428 | ENABLE_NS_ASSERTIONS = NO; 1429 | ENABLE_STRICT_OBJC_MSGSEND = YES; 1430 | GCC_C_LANGUAGE_STANDARD = gnu11; 1431 | GCC_NO_COMMON_BLOCKS = YES; 1432 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 1433 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 1434 | GCC_WARN_UNDECLARED_SELECTOR = YES; 1435 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 1436 | GCC_WARN_UNUSED_FUNCTION = YES; 1437 | GCC_WARN_UNUSED_VARIABLE = YES; 1438 | INFOPLIST_FILE = PokemonCardsSnapshotTests/Info.plist; 1439 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 1440 | LD_RUNPATH_SEARCH_PATHS = ( 1441 | "$(inherited)", 1442 | "@executable_path/Frameworks", 1443 | "@loader_path/Frameworks", 1444 | ); 1445 | MTL_ENABLE_DEBUG_INFO = NO; 1446 | MTL_FAST_MATH = YES; 1447 | PRODUCT_BUNDLE_IDENTIFIER = Coletiv.PokemonCardsSnapshotTests; 1448 | PRODUCT_NAME = "$(TARGET_NAME)"; 1449 | SDKROOT = iphoneos; 1450 | SWIFT_COMPILATION_MODE = wholemodule; 1451 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 1452 | SWIFT_VERSION = 5.0; 1453 | TARGETED_DEVICE_FAMILY = "1,2"; 1454 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PokemonCards.app/PokemonCards"; 1455 | VALIDATE_PRODUCT = YES; 1456 | }; 1457 | name = "Release-Staging"; 1458 | }; 1459 | 10E404AD25706D6900B1FA1C /* Debug-Staging */ = { 1460 | isa = XCBuildConfiguration; 1461 | baseConfigurationReference = 10E379B62570049A00B2CFBE /* Staging.xcconfig */; 1462 | buildSettings = { 1463 | ALWAYS_SEARCH_USER_PATHS = NO; 1464 | BUNDLE_LOADER = "$(TEST_HOST)"; 1465 | CLANG_ANALYZER_NONNULL = YES; 1466 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 1467 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 1468 | CLANG_CXX_LIBRARY = "libc++"; 1469 | CLANG_ENABLE_MODULES = YES; 1470 | CLANG_ENABLE_OBJC_ARC = YES; 1471 | CLANG_ENABLE_OBJC_WEAK = YES; 1472 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 1473 | CLANG_WARN_BOOL_CONVERSION = YES; 1474 | CLANG_WARN_COMMA = YES; 1475 | CLANG_WARN_CONSTANT_CONVERSION = YES; 1476 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 1477 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 1478 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 1479 | CLANG_WARN_EMPTY_BODY = YES; 1480 | CLANG_WARN_ENUM_CONVERSION = YES; 1481 | CLANG_WARN_INFINITE_RECURSION = YES; 1482 | CLANG_WARN_INT_CONVERSION = YES; 1483 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 1484 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 1485 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 1486 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 1487 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 1488 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 1489 | CLANG_WARN_STRICT_PROTOTYPES = YES; 1490 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 1491 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 1492 | CLANG_WARN_UNREACHABLE_CODE = YES; 1493 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 1494 | CODE_SIGN_STYLE = Automatic; 1495 | COPY_PHASE_STRIP = NO; 1496 | DEBUG_INFORMATION_FORMAT = dwarf; 1497 | DEVELOPMENT_TEAM = RGK82JLN9X; 1498 | ENABLE_STRICT_OBJC_MSGSEND = YES; 1499 | ENABLE_TESTABILITY = YES; 1500 | GCC_C_LANGUAGE_STANDARD = gnu11; 1501 | GCC_DYNAMIC_NO_PIC = NO; 1502 | GCC_NO_COMMON_BLOCKS = YES; 1503 | GCC_OPTIMIZATION_LEVEL = 0; 1504 | GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; 1505 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 1506 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 1507 | GCC_WARN_UNDECLARED_SELECTOR = YES; 1508 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 1509 | GCC_WARN_UNUSED_FUNCTION = YES; 1510 | GCC_WARN_UNUSED_VARIABLE = YES; 1511 | INFOPLIST_FILE = PokemonCardsTests/Info.plist; 1512 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 1513 | LD_RUNPATH_SEARCH_PATHS = ( 1514 | "$(inherited)", 1515 | "@executable_path/Frameworks", 1516 | "@loader_path/Frameworks", 1517 | ); 1518 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 1519 | MTL_FAST_MATH = YES; 1520 | ONLY_ACTIVE_ARCH = YES; 1521 | PRODUCT_BUNDLE_IDENTIFIER = Coletiv.PokemonCardsTests; 1522 | PRODUCT_NAME = "$(TARGET_NAME)"; 1523 | SDKROOT = iphoneos; 1524 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 1525 | SWIFT_COMPILATION_MODE = singlefile; 1526 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 1527 | SWIFT_VERSION = 5.0; 1528 | TARGETED_DEVICE_FAMILY = "1,2"; 1529 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PokemonCards.app/PokemonCards"; 1530 | }; 1531 | name = "Debug-Staging"; 1532 | }; 1533 | 10E404AE25706D6900B1FA1C /* Release-Production */ = { 1534 | isa = XCBuildConfiguration; 1535 | buildSettings = { 1536 | ALWAYS_SEARCH_USER_PATHS = NO; 1537 | BUNDLE_LOADER = "$(TEST_HOST)"; 1538 | CLANG_ANALYZER_NONNULL = YES; 1539 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 1540 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 1541 | CLANG_CXX_LIBRARY = "libc++"; 1542 | CLANG_ENABLE_MODULES = YES; 1543 | CLANG_ENABLE_OBJC_ARC = YES; 1544 | CLANG_ENABLE_OBJC_WEAK = YES; 1545 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 1546 | CLANG_WARN_BOOL_CONVERSION = YES; 1547 | CLANG_WARN_COMMA = YES; 1548 | CLANG_WARN_CONSTANT_CONVERSION = YES; 1549 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 1550 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 1551 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 1552 | CLANG_WARN_EMPTY_BODY = YES; 1553 | CLANG_WARN_ENUM_CONVERSION = YES; 1554 | CLANG_WARN_INFINITE_RECURSION = YES; 1555 | CLANG_WARN_INT_CONVERSION = YES; 1556 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 1557 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 1558 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 1559 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 1560 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 1561 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 1562 | CLANG_WARN_STRICT_PROTOTYPES = YES; 1563 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 1564 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 1565 | CLANG_WARN_UNREACHABLE_CODE = YES; 1566 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 1567 | CODE_SIGN_STYLE = Automatic; 1568 | COPY_PHASE_STRIP = NO; 1569 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 1570 | DEVELOPMENT_TEAM = RGK82JLN9X; 1571 | ENABLE_NS_ASSERTIONS = NO; 1572 | ENABLE_STRICT_OBJC_MSGSEND = YES; 1573 | GCC_C_LANGUAGE_STANDARD = gnu11; 1574 | GCC_NO_COMMON_BLOCKS = YES; 1575 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 1576 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 1577 | GCC_WARN_UNDECLARED_SELECTOR = YES; 1578 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 1579 | GCC_WARN_UNUSED_FUNCTION = YES; 1580 | GCC_WARN_UNUSED_VARIABLE = YES; 1581 | INFOPLIST_FILE = PokemonCardsTests/Info.plist; 1582 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 1583 | LD_RUNPATH_SEARCH_PATHS = ( 1584 | "$(inherited)", 1585 | "@executable_path/Frameworks", 1586 | "@loader_path/Frameworks", 1587 | ); 1588 | MTL_ENABLE_DEBUG_INFO = NO; 1589 | MTL_FAST_MATH = YES; 1590 | PRODUCT_BUNDLE_IDENTIFIER = Coletiv.PokemonCardsTests; 1591 | PRODUCT_NAME = "$(TARGET_NAME)"; 1592 | SDKROOT = iphoneos; 1593 | SWIFT_COMPILATION_MODE = wholemodule; 1594 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 1595 | SWIFT_VERSION = 5.0; 1596 | TARGETED_DEVICE_FAMILY = "1,2"; 1597 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PokemonCards.app/PokemonCards"; 1598 | VALIDATE_PRODUCT = YES; 1599 | }; 1600 | name = "Release-Production"; 1601 | }; 1602 | /* End XCBuildConfiguration section */ 1603 | 1604 | /* Begin XCConfigurationList section */ 1605 | 10CD45AF23C4E58600E0D0D4 /* Build configuration list for PBXProject "PokemonCards" */ = { 1606 | isa = XCConfigurationList; 1607 | buildConfigurations = ( 1608 | 10CD45DC23C4E58700E0D0D4 /* Debug-Staging */, 1609 | 10D3C1372570FF290031735E /* Debug-Production */, 1610 | 10CD45DD23C4E58700E0D0D4 /* Release-Production */, 1611 | 10D3C13A2570FF4B0031735E /* Release-Staging */, 1612 | ); 1613 | defaultConfigurationIsVisible = 0; 1614 | defaultConfigurationName = "Release-Production"; 1615 | }; 1616 | 10CD45DE23C4E58700E0D0D4 /* Build configuration list for PBXNativeTarget "PokemonCards" */ = { 1617 | isa = XCConfigurationList; 1618 | buildConfigurations = ( 1619 | 10CD45DF23C4E58700E0D0D4 /* Debug-Staging */, 1620 | 10D3C1382570FF290031735E /* Debug-Production */, 1621 | 10CD45E023C4E58700E0D0D4 /* Release-Production */, 1622 | 10D3C13B2570FF4B0031735E /* Release-Staging */, 1623 | ); 1624 | defaultConfigurationIsVisible = 0; 1625 | defaultConfigurationName = "Release-Production"; 1626 | }; 1627 | 10D6347E258A72A4009CB28D /* Build configuration list for PBXNativeTarget "PokemonCardsSnapshotTests" */ = { 1628 | isa = XCConfigurationList; 1629 | buildConfigurations = ( 1630 | 10D6347A258A72A3009CB28D /* Debug-Staging */, 1631 | 10D6347B258A72A3009CB28D /* Debug-Production */, 1632 | 10D6347C258A72A3009CB28D /* Release-Production */, 1633 | 10D6347D258A72A3009CB28D /* Release-Staging */, 1634 | ); 1635 | defaultConfigurationIsVisible = 0; 1636 | defaultConfigurationName = "Release-Production"; 1637 | }; 1638 | 10E404AC25706D6900B1FA1C /* Build configuration list for PBXNativeTarget "PokemonCardsTests" */ = { 1639 | isa = XCConfigurationList; 1640 | buildConfigurations = ( 1641 | 10E404AD25706D6900B1FA1C /* Debug-Staging */, 1642 | 10D3C1392570FF290031735E /* Debug-Production */, 1643 | 10E404AE25706D6900B1FA1C /* Release-Production */, 1644 | 10D3C13C2570FF4B0031735E /* Release-Staging */, 1645 | ); 1646 | defaultConfigurationIsVisible = 0; 1647 | defaultConfigurationName = "Release-Production"; 1648 | }; 1649 | /* End XCConfigurationList section */ 1650 | 1651 | /* Begin XCRemoteSwiftPackageReference section */ 1652 | 10D3D0902577B9AC0025C730 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { 1653 | isa = XCRemoteSwiftPackageReference; 1654 | repositoryURL = "https://github.com/onevcat/Kingfisher.git"; 1655 | requirement = { 1656 | kind = upToNextMajorVersion; 1657 | minimumVersion = 5.15.8; 1658 | }; 1659 | }; 1660 | 10DFBEE8258A106F0078C982 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = { 1661 | isa = XCRemoteSwiftPackageReference; 1662 | repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing.git"; 1663 | requirement = { 1664 | kind = upToNextMajorVersion; 1665 | minimumVersion = 1.8.2; 1666 | }; 1667 | }; 1668 | 10E4047125706B0900B1FA1C /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { 1669 | isa = XCRemoteSwiftPackageReference; 1670 | repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture"; 1671 | requirement = { 1672 | kind = upToNextMajorVersion; 1673 | minimumVersion = 0.9.0; 1674 | }; 1675 | }; 1676 | /* End XCRemoteSwiftPackageReference section */ 1677 | 1678 | /* Begin XCSwiftPackageProductDependency section */ 1679 | 10D3D0912577B9AC0025C730 /* KingfisherSwiftUI */ = { 1680 | isa = XCSwiftPackageProductDependency; 1681 | package = 10D3D0902577B9AC0025C730 /* XCRemoteSwiftPackageReference "Kingfisher" */; 1682 | productName = KingfisherSwiftUI; 1683 | }; 1684 | 10D6349A258A7317009CB28D /* SnapshotTesting */ = { 1685 | isa = XCSwiftPackageProductDependency; 1686 | package = 10DFBEE8258A106F0078C982 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; 1687 | productName = SnapshotTesting; 1688 | }; 1689 | 10D6349C258A7321009CB28D /* SnapshotTesting */ = { 1690 | isa = XCSwiftPackageProductDependency; 1691 | package = 10DFBEE8258A106F0078C982 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; 1692 | productName = SnapshotTesting; 1693 | }; 1694 | 10E4047225706B0900B1FA1C /* ComposableArchitecture */ = { 1695 | isa = XCSwiftPackageProductDependency; 1696 | package = 10E4047125706B0900B1FA1C /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; 1697 | productName = ComposableArchitecture; 1698 | }; 1699 | /* End XCSwiftPackageProductDependency section */ 1700 | 1701 | /* Begin XCVersionGroup section */ 1702 | 10B2D6A9258381BC00A9C65D /* PokemonCards.xcdatamodeld */ = { 1703 | isa = XCVersionGroup; 1704 | children = ( 1705 | 10B2D6AA258381BC00A9C65D /* Cards.xcdatamodel */, 1706 | ); 1707 | currentVersion = 10B2D6AA258381BC00A9C65D /* Cards.xcdatamodel */; 1708 | path = PokemonCards.xcdatamodeld; 1709 | sourceTree = ""; 1710 | versionGroupType = wrapper.xcdatamodel; 1711 | }; 1712 | /* End XCVersionGroup section */ 1713 | }; 1714 | rootObject = 10CD45AC23C4E58600E0D0D4 /* Project object */; 1715 | } 1716 | -------------------------------------------------------------------------------- /PokemonCards.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PokemonCards.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PokemonCards.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | PreviewsEnabled 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /PokemonCards.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "combine-schedulers", 6 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 7 | "state": { 8 | "branch": null, 9 | "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", 10 | "version": "0.5.3" 11 | } 12 | }, 13 | { 14 | "package": "Kingfisher", 15 | "repositoryURL": "https://github.com/onevcat/Kingfisher.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "1a0c2df04b31ed7aa318354f3583faea24f006fc", 19 | "version": "5.15.8" 20 | } 21 | }, 22 | { 23 | "package": "swift-case-paths", 24 | "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", 25 | "state": { 26 | "branch": null, 27 | "revision": "241301b67d8551c26d8f09bd2c0e52cc49f18007", 28 | "version": "0.8.0" 29 | } 30 | }, 31 | { 32 | "package": "swift-collections", 33 | "repositoryURL": "https://github.com/apple/swift-collections", 34 | "state": { 35 | "branch": null, 36 | "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", 37 | "version": "1.0.2" 38 | } 39 | }, 40 | { 41 | "package": "swift-composable-architecture", 42 | "repositoryURL": "https://github.com/pointfreeco/swift-composable-architecture", 43 | "state": { 44 | "branch": null, 45 | "revision": "ba9c626ab1b2b6af8cf684eebb2ab472fa5b6753", 46 | "version": "0.33.1" 47 | } 48 | }, 49 | { 50 | "package": "swift-custom-dump", 51 | "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", 52 | "state": { 53 | "branch": null, 54 | "revision": "51698ece74ecf31959d3fa81733f0a5363ef1b4e", 55 | "version": "0.3.0" 56 | } 57 | }, 58 | { 59 | "package": "swift-identified-collections", 60 | "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", 61 | "state": { 62 | "branch": null, 63 | "revision": "680bf440178a78a627b1c2c64c0855f6523ad5b9", 64 | "version": "0.3.2" 65 | } 66 | }, 67 | { 68 | "package": "SnapshotTesting", 69 | "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "f8a9c997c3c1dab4e216a8ec9014e23144cbab37", 73 | "version": "1.9.0" 74 | } 75 | }, 76 | { 77 | "package": "xctest-dynamic-overlay", 78 | "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", 79 | "state": { 80 | "branch": null, 81 | "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", 82 | "version": "0.2.1" 83 | } 84 | } 85 | ] 86 | }, 87 | "version": 1 88 | } 89 | -------------------------------------------------------------------------------- /PokemonCards.xcodeproj/xcshareddata/xcschemes/Production.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 78 | 79 | 80 | 81 | 87 | 89 | 95 | 96 | 97 | 98 | 100 | 101 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /PokemonCards.xcodeproj/xcshareddata/xcschemes/Staging.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /PokemonCards/Application/CardDetail/CardDetailCore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardDetailCore.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 02/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | 11 | struct CardDetailState: Equatable, Identifiable { 12 | let id: UUID 13 | var card: Card 14 | 15 | var isFavorite = false 16 | var favorites = [Card]() 17 | } 18 | 19 | enum CardDetailAction: Equatable { 20 | case onAppear 21 | case onDisappear 22 | 23 | case toggleFavorite 24 | case favoritesResponse(Result<[Card], Never>) 25 | case toggleFavoriteResponse(Result<[Card], Never>) 26 | } 27 | 28 | struct CardDetailEnvironment { 29 | var favoriteCardsClient: FavoriteCardsClient 30 | var mainQueue: AnySchedulerOf 31 | } 32 | 33 | // MARK: - Reducer 34 | 35 | let cardDetailReducer = 36 | Reducer { state, action, environment in 37 | 38 | struct CardDetailCancelId: Hashable {} 39 | 40 | switch action { 41 | case .onAppear: 42 | return environment.favoriteCardsClient 43 | .all() 44 | .receive(on: environment.mainQueue) 45 | .catchToEffect() 46 | .map(CardDetailAction.favoritesResponse) 47 | .cancellable(id: CardDetailCancelId()) 48 | 49 | case .favoritesResponse(.success(let favorites)), 50 | .toggleFavoriteResponse(.success(let favorites)): 51 | state.favorites = favorites 52 | state.isFavorite = favorites.contains(where: { $0.id == state.card.id }) 53 | return .none 54 | 55 | case .toggleFavorite: 56 | if state.isFavorite { 57 | return environment.favoriteCardsClient 58 | .remove(state.card) 59 | .receive(on: environment.mainQueue) 60 | .catchToEffect() 61 | .map(CardDetailAction.toggleFavoriteResponse) 62 | .cancellable(id: CardDetailCancelId()) 63 | } else { 64 | return environment.favoriteCardsClient 65 | .add(state.card) 66 | .receive(on: environment.mainQueue) 67 | .catchToEffect() 68 | .map(CardDetailAction.toggleFavoriteResponse) 69 | .cancellable(id: CardDetailCancelId()) 70 | } 71 | 72 | case .onDisappear: 73 | return .cancel(id: CardDetailCancelId()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /PokemonCards/Application/CardDetail/CardDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardDetailView.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 03/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | import KingfisherSwiftUI 11 | import SwiftUI 12 | 13 | struct CardDetailView: View { 14 | let store: Store 15 | 16 | var body: some View { 17 | WithViewStore(store) { viewStore in 18 | VStack { 19 | KFImage(viewStore.card.images.smallURL) 20 | .placeholder { 21 | ActivityIndicator( 22 | style: .large, 23 | isAnimating: .constant(true) 24 | ) 25 | } 26 | .resizable() 27 | .aspectRatio(CGSize(width: 600, height: 825), contentMode: .fit) 28 | .clipped() 29 | .padding(.horizontal, 16) 30 | Spacer() 31 | } 32 | .padding(.top, 32) 33 | .navigationBarTitle(viewStore.card.name) 34 | .navigationBarItems(trailing: favoriteButton(viewStore)) 35 | .onAppear { viewStore.send(.onAppear) } 36 | .onDisappear { viewStore.send(.onDisappear) } 37 | } 38 | } 39 | } 40 | 41 | // MARK: - Views 42 | 43 | extension CardDetailView { 44 | @ViewBuilder 45 | private func favoriteButton(_ viewStore: ViewStore) -> some View { 46 | WithViewStore(store.scope(state: { $0.isFavorite })) { favoriteViewStore in 47 | FavoriteButton( 48 | action: { viewStore.send(.toggleFavorite) }, 49 | isFavorite: favoriteViewStore.state 50 | ) 51 | } 52 | } 53 | } 54 | 55 | // MARK: - Previews 56 | 57 | struct CardDetailView_Previews: PreviewProvider { 58 | static var previews: some View { 59 | NavigationView { 60 | CardDetailView( 61 | store: .init( 62 | initialState: CardDetailState( 63 | id: .init(), 64 | card: .mock1 65 | ), 66 | reducer: cardDetailReducer, 67 | environment: .init( 68 | favoriteCardsClient: .mockPreview(), 69 | mainQueue: DispatchQueue.main.eraseToAnyScheduler() 70 | ) 71 | ) 72 | ) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /PokemonCards/Application/Cards/CardsCore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardsCore.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 26/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | 11 | struct CardsState: Equatable { 12 | var cards = IdentifiedArrayOf() 13 | var favorites = [Card]() 14 | 15 | var currentPage = 1 16 | let pageSize = 15 17 | var isLoading = false 18 | var isLoadingPage = false 19 | 20 | // HELPER 21 | 22 | func isFavorite(with card: Card) -> Bool { 23 | return favorites.contains(where: { $0.id == card.id }) 24 | } 25 | func isLastItem(_ item: UUID) -> Bool { 26 | let itemIndex = cards.firstIndex(where: { $0.id == item }) 27 | return itemIndex == cards.endIndex - 1 28 | } 29 | } 30 | 31 | enum CardsAction: Equatable { 32 | case retrieve 33 | case retrieveNextPageIfNeeded(currentItem: UUID) 34 | case cardsResponse(Result) 35 | 36 | case retrieveFavorites 37 | case favoritesResponse(Result<[Card], Never>) 38 | 39 | case loadingActive(Bool) 40 | case loadingPageActive(Bool) 41 | 42 | case card(id: UUID, action: CardDetailAction) 43 | 44 | case onAppear 45 | case onDisappear 46 | } 47 | 48 | struct CardsEnvironment { 49 | var cardsClient: CardsClient 50 | var favoriteCardsClient: FavoriteCardsClient 51 | var mainQueue: AnySchedulerOf 52 | var uuid: () -> UUID 53 | } 54 | 55 | // MARK: - Reducer 56 | 57 | let cardsReducer = 58 | Reducer.combine( 59 | cardDetailReducer.forEach( 60 | state: \.cards, 61 | action: /CardsAction.card(id:action:), 62 | environment: { environment in 63 | .init( 64 | favoriteCardsClient: .live, 65 | mainQueue: environment.mainQueue 66 | ) 67 | } 68 | ), 69 | .init { state, action, environment in 70 | 71 | struct CardsCancelId: Hashable {} 72 | 73 | switch action { 74 | case .onAppear: 75 | guard state.cards.isEmpty else { return .init(value: .retrieveFavorites) } 76 | return .init(value: .retrieve) 77 | 78 | case .retrieve: 79 | state.cards = [] 80 | state.currentPage = 1 81 | return .concatenate( 82 | .init(value: .loadingActive(true)), 83 | environment.cardsClient 84 | .page(state.currentPage, state.pageSize) 85 | .receive(on: environment.mainQueue) 86 | .catchToEffect() 87 | .map(CardsAction.cardsResponse) 88 | .cancellable(id: CardsCancelId()), 89 | .init(value: .retrieveFavorites) 90 | ) 91 | 92 | case .retrieveNextPageIfNeeded(currentItem: let item): 93 | guard 94 | state.isLastItem(item), 95 | !state.isLoadingPage 96 | else { return .none } 97 | 98 | state.currentPage += 1 99 | return .concatenate( 100 | .init(value: .loadingPageActive(true)), 101 | environment.cardsClient 102 | .page(state.currentPage, state.pageSize) 103 | .receive(on: environment.mainQueue) 104 | .catchToEffect() 105 | .map(CardsAction.cardsResponse) 106 | .cancellable(id: CardsCancelId()) 107 | ) 108 | 109 | case .retrieveFavorites: 110 | return environment.favoriteCardsClient 111 | .all() 112 | .receive(on: environment.mainQueue) 113 | .catchToEffect() 114 | .map(CardsAction.favoritesResponse) 115 | .cancellable(id: CardsCancelId()) 116 | 117 | case .cardsResponse(.success(let cards)): 118 | cards.cards.forEach { 119 | state.cards.append(CardDetailState( 120 | id: environment.uuid(), 121 | card: $0 122 | )) 123 | } 124 | 125 | return .concatenate( 126 | .init(value: .loadingActive(false)), 127 | .init(value: .loadingPageActive(false)) 128 | ) 129 | 130 | case .cardsResponse(.failure(let error)): 131 | return .concatenate( 132 | .init(value: .loadingActive(false)), 133 | .init(value: .loadingPageActive(false)) 134 | ) 135 | 136 | case .favoritesResponse(.success(let favorites)): 137 | state.favorites = favorites 138 | return .none 139 | 140 | case .loadingActive(let isLoading): 141 | state.isLoading = isLoading 142 | return .none 143 | 144 | case .loadingPageActive(let isLoading): 145 | state.isLoadingPage = isLoading 146 | return .none 147 | 148 | case .card(id: _, action: .onDisappear): 149 | return .init(value: .retrieveFavorites) 150 | 151 | case .card(id: _, action: _): 152 | return .none 153 | 154 | case .onDisappear: 155 | return .cancel(id: CardsCancelId()) 156 | } 157 | } 158 | ) 159 | -------------------------------------------------------------------------------- /PokemonCards/Application/Cards/CardsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardsView.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 26/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | import SwiftUI 11 | 12 | struct CardsView: View { 13 | var store: Store 14 | 15 | var body: some View { 16 | WithViewStore(store) { viewStore in 17 | NavigationView { 18 | ScrollView { 19 | Group { 20 | if viewStore.isLoading { 21 | VStack { 22 | Spacer() 23 | ActivityIndicator( 24 | style: .large, 25 | isAnimating: viewStore.binding( 26 | get: { $0.isLoading }, 27 | send: CardsAction.loadingActive 28 | ) 29 | ) 30 | Spacer() 31 | } 32 | } else { 33 | VStack { 34 | itemsList(viewStore) 35 | ActivityIndicator( 36 | style: .medium, 37 | isAnimating: viewStore.binding( 38 | get: { $0.isLoadingPage }, 39 | send: CardsAction.loadingPageActive 40 | ) 41 | ) 42 | } 43 | } 44 | } 45 | .padding() 46 | } 47 | .edgesIgnoringSafeArea(.bottom) 48 | .navigationBarTitle(Localization.Cards.title) 49 | } 50 | .onAppear { viewStore.send(.onAppear) } 51 | .onDisappear { viewStore.send(.onDisappear) } 52 | } 53 | } 54 | } 55 | 56 | // MARK: - Views 57 | 58 | extension CardsView { 59 | @ViewBuilder 60 | private func itemsList(_ viewStore: ViewStore) -> some View { 61 | if #available(iOS 14.0, *) { 62 | let gridItem = GridItem(.flexible(minimum: 80, maximum: 180)) 63 | LazyVGrid( 64 | columns: [gridItem, gridItem, gridItem], 65 | alignment: .center, 66 | spacing: 16, 67 | content: { cardsList(viewStore) } 68 | ) 69 | } else { 70 | VStack { 71 | cardsList(viewStore) 72 | } 73 | } 74 | } 75 | 76 | @ViewBuilder 77 | private func cardsList(_ viewStore: ViewStore) -> some View { 78 | ForEachStore( 79 | store.scope( 80 | state: { $0.cards }, 81 | action: CardsAction.card(id:action:) 82 | ), 83 | content: { cardStore in 84 | WithViewStore(cardStore) { cardViewStore in 85 | NavigationLink( 86 | destination: CardDetailView(store: cardStore), 87 | label: { 88 | CardItemView( 89 | card: cardViewStore.state.card, 90 | isFavorite: viewStore.state.isFavorite(with: cardViewStore.state.card) 91 | ) 92 | .onAppear { 93 | viewStore.send(.retrieveNextPageIfNeeded(currentItem: cardViewStore.state.id)) 94 | } 95 | } 96 | ) 97 | } 98 | } 99 | ) 100 | } 101 | } 102 | 103 | // MARK: - Previews 104 | 105 | struct CardsView_Previews: PreviewProvider { 106 | static var previews: some View { 107 | ForEach(["en", "pt_PT"], id: \.self) { id in 108 | CardsView( 109 | store: .init( 110 | initialState: .init( 111 | cards: .init( 112 | uniqueElements: Cards.mock.cards.map { 113 | CardDetailState( 114 | id: .init(), 115 | card: $0 116 | ) 117 | } 118 | ) 119 | ), 120 | reducer: cardsReducer, 121 | environment: .init( 122 | cardsClient: .mockPreview(), 123 | favoriteCardsClient: .mockPreview(), 124 | mainQueue: DispatchQueue.main.eraseToAnyScheduler(), 125 | uuid: UUID.init 126 | ) 127 | ) 128 | ) 129 | .environment(\.locale, .init(identifier: id)) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /PokemonCards/Application/Favorites/FavoritesCore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoritesCore.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 26/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | 11 | struct FavoritesState: Equatable { 12 | var cards = IdentifiedArrayOf() 13 | } 14 | 15 | enum FavoritesAction: Equatable { 16 | case retrieveFavorites 17 | case favoritesResponse(Result<[Card], Never>) 18 | 19 | case card(id: UUID, action: CardDetailAction) 20 | 21 | case onAppear 22 | case onDisappear 23 | } 24 | 25 | struct FavoritesEnvironment { 26 | var favoriteCardsClient: FavoriteCardsClient 27 | var mainQueue: AnySchedulerOf 28 | var uuid: () -> UUID 29 | } 30 | 31 | // MARK: - Reducer 32 | 33 | let favoritesReducer = 34 | Reducer.combine( 35 | cardDetailReducer.forEach( 36 | state: \.cards, 37 | action: /FavoritesAction.card(id:action:), 38 | environment: { environment in 39 | .init( 40 | favoriteCardsClient: environment.favoriteCardsClient, 41 | mainQueue: environment.mainQueue 42 | ) 43 | } 44 | ), 45 | .init { state, action, environment in 46 | 47 | struct FavoritesCancelId: Hashable {} 48 | 49 | switch action { 50 | case .onAppear: 51 | guard state.cards.isEmpty else { return .none } 52 | return .init(value: .retrieveFavorites) 53 | 54 | case .retrieveFavorites: 55 | return environment.favoriteCardsClient 56 | .all() 57 | .receive(on: environment.mainQueue) 58 | .catchToEffect() 59 | .map(FavoritesAction.favoritesResponse) 60 | .cancellable(id: FavoritesCancelId()) 61 | 62 | case .favoritesResponse(.success(let favorites)): 63 | state.cards = .init( 64 | uniqueElements: favorites.map { 65 | CardDetailState( 66 | id: environment.uuid(), 67 | card: $0 68 | ) 69 | } 70 | ) 71 | return .none 72 | 73 | case .card(id: _, action: .onDisappear): 74 | return .init(value: .retrieveFavorites) 75 | 76 | case .card(id: _, action: _): 77 | return .none 78 | 79 | case .onDisappear: 80 | return .cancel(id: FavoritesCancelId()) 81 | } 82 | } 83 | ) 84 | -------------------------------------------------------------------------------- /PokemonCards/Application/Favorites/FavoritesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoritesView.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 26/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | import SwiftUI 11 | 12 | struct FavoritesView: View { 13 | var store: Store 14 | 15 | var body: some View { 16 | WithViewStore(store) { viewStore in 17 | NavigationView { 18 | ScrollView { 19 | itemsList(viewStore) 20 | .padding() 21 | } 22 | .edgesIgnoringSafeArea(.bottom) 23 | .navigationBarTitle(Localization.Cards.title) 24 | } 25 | .onAppear { viewStore.send(.onAppear) } 26 | .onDisappear { viewStore.send(.onDisappear) } 27 | } 28 | } 29 | } 30 | 31 | // MARK: - Views 32 | 33 | extension FavoritesView { 34 | @ViewBuilder 35 | private func itemsList(_ viewStore: ViewStore) -> some View { 36 | if #available(iOS 14.0, *) { 37 | let gridItem = GridItem(.flexible(minimum: 80, maximum: 180)) 38 | LazyVGrid( 39 | columns: [gridItem, gridItem, gridItem], 40 | alignment: .center, 41 | spacing: 16, 42 | content: { cardsList(viewStore) } 43 | ) 44 | } else { 45 | VStack { 46 | cardsList(viewStore) 47 | } 48 | } 49 | } 50 | 51 | @ViewBuilder 52 | private func cardsList(_ viewStore: ViewStore) -> some View { 53 | ForEachStore( 54 | store.scope( 55 | state: { $0.cards }, 56 | action: FavoritesAction.card(id:action:) 57 | ), 58 | content: { cardStore in 59 | WithViewStore(cardStore) { cardViewStore in 60 | NavigationLink( 61 | destination: CardDetailView(store: cardStore), 62 | label: { 63 | CardItemView( 64 | card: cardViewStore.state.card, 65 | isFavorite: true 66 | ) 67 | } 68 | ) 69 | } 70 | } 71 | ) 72 | } 73 | } 74 | 75 | // MARK: - Previews 76 | 77 | struct FavoritesView_Previews: PreviewProvider { 78 | static var previews: some View { 79 | ForEach(["en", "pt_PT"], id: \.self) { id in 80 | FavoritesView( 81 | store: .init( 82 | initialState: .init( 83 | cards: .init( 84 | uniqueElements: Cards.mock.cards.map { 85 | CardDetailState( 86 | id: .init(), 87 | card: $0 88 | ) 89 | } 90 | ) 91 | ), 92 | reducer: favoritesReducer, 93 | environment: .init( 94 | favoriteCardsClient: .mockPreview(), 95 | mainQueue: DispatchQueue.main.eraseToAnyScheduler(), 96 | uuid: UUID.init 97 | ) 98 | ) 99 | ) 100 | .environment(\.locale, .init(identifier: id)) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /PokemonCards/Application/Launch/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 07/01/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | return true 15 | } 16 | 17 | // MARK: UISceneSession Lifecycle 18 | 19 | func application( 20 | _ application: UIApplication, 21 | configurationForConnecting connectingSceneSession: UISceneSession, 22 | options: UIScene.ConnectionOptions 23 | ) -> UISceneConfiguration { 24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {} 28 | } 29 | -------------------------------------------------------------------------------- /PokemonCards/Application/Launch/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /PokemonCards/Application/Launch/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 07/01/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import UIKit 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | var window: UIWindow? 14 | 15 | func scene( 16 | _ scene: UIScene, 17 | willConnectTo session: UISceneSession, 18 | options connectionOptions: UIScene.ConnectionOptions 19 | ) { 20 | let mainView = MainView( 21 | store: .init( 22 | initialState: .init(), 23 | reducer: mainReducer, 24 | environment: .init( 25 | cardsClient: .live, 26 | favoriteCardsClient: .live, 27 | mainQueue: DispatchQueue.main.eraseToAnyScheduler(), 28 | uuid: UUID.init 29 | ) 30 | ) 31 | ) 32 | 33 | if let windowScene = scene as? UIWindowScene { 34 | let window = UIWindow(windowScene: windowScene) 35 | window.rootViewController = UIHostingController(rootView: mainView) 36 | self.window = window 37 | window.makeKeyAndVisible() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /PokemonCards/Application/Launch/en.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /PokemonCards/Application/Launch/pt-PT.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /PokemonCards/Application/Main/MainCore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainCore.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 17/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | 11 | struct MainState: Equatable { 12 | var cardsState = CardsState() 13 | var favoritesState = FavoritesState() 14 | 15 | enum Tab { 16 | case cards 17 | case favorites 18 | } 19 | 20 | var selectedTab = Tab.cards 21 | } 22 | 23 | enum MainAction { 24 | case cards(CardsAction) 25 | case favorites(FavoritesAction) 26 | 27 | case selectedTabChange(MainState.Tab) 28 | } 29 | 30 | struct MainEnvironment { 31 | var cardsClient: CardsClient 32 | var favoriteCardsClient: FavoriteCardsClient 33 | var mainQueue: AnySchedulerOf 34 | var uuid: () -> UUID 35 | } 36 | 37 | // MARK: - Reducer 38 | 39 | let mainReducer: Reducer = .combine( 40 | cardsReducer.pullback( 41 | state: \MainState.cardsState, 42 | action: /MainAction.cards, 43 | environment: { environment in 44 | CardsEnvironment( 45 | cardsClient: environment.cardsClient, 46 | favoriteCardsClient: environment.favoriteCardsClient, 47 | mainQueue: environment.mainQueue, 48 | uuid: environment.uuid 49 | ) 50 | } 51 | ), 52 | favoritesReducer.pullback( 53 | state: \MainState.favoritesState, 54 | action: /MainAction.favorites, 55 | environment: { environment in 56 | FavoritesEnvironment( 57 | favoriteCardsClient: environment.favoriteCardsClient, 58 | mainQueue: environment.mainQueue, 59 | uuid: environment.uuid 60 | ) 61 | } 62 | ), 63 | .init { state, action, environment in 64 | 65 | switch action { 66 | // Update favorites on Cards State 67 | case .cards(.card(id: _, action: .toggleFavoriteResponse(.success(let favorites)))): 68 | state.favoritesState.cards = .init( 69 | uniqueElements: favorites.map { 70 | CardDetailState( 71 | id: environment.uuid(), 72 | card: $0 73 | ) 74 | } 75 | ) 76 | return .none 77 | 78 | case .cards: 79 | return .none 80 | 81 | // Update favorites on Favorites State 82 | case .favorites(.card(id: _, action: .toggleFavoriteResponse(.success(let favorites)))): 83 | state.cardsState.favorites = favorites 84 | return .none 85 | 86 | case .favorites: 87 | return .none 88 | 89 | case .selectedTabChange(let selectedTab): 90 | state.selectedTab = selectedTab 91 | return .none 92 | } 93 | } 94 | ) 95 | -------------------------------------------------------------------------------- /PokemonCards/Application/Main/MainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainView.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 17/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | import SwiftUI 11 | 12 | struct MainView: View { 13 | let store: Store 14 | 15 | var body: some View { 16 | WithViewStore(store) { viewStore in 17 | TabView( 18 | selection: viewStore.binding( 19 | get: { $0.selectedTab }, 20 | send: MainAction.selectedTabChange 21 | ), 22 | content: { 23 | Group { 24 | CardsView(store: cardsStore) 25 | .tabItem { 26 | Image(systemName: "greetingcard") 27 | Text(Localization.Cards.title) 28 | } 29 | .tag(MainState.Tab.cards) 30 | FavoritesView(store: favoritesStore) 31 | .tabItem { 32 | Image(systemName: "star") 33 | Text(Localization.Favorites.title) 34 | } 35 | .tag(MainState.Tab.favorites) 36 | } 37 | } 38 | ) 39 | } 40 | } 41 | } 42 | 43 | // MARK: - Store inits 44 | 45 | extension MainView { 46 | private var cardsStore: Store { 47 | return store.scope( 48 | state: { $0.cardsState }, 49 | action: MainAction.cards 50 | ) 51 | } 52 | 53 | private var favoritesStore: Store { 54 | return store.scope( 55 | state: { $0.favoritesState }, 56 | action: MainAction.favorites 57 | ) 58 | } 59 | } 60 | 61 | // MARK: - Previews 62 | 63 | struct MainView_Previews: PreviewProvider { 64 | static var previews: some View { 65 | MainView( 66 | store: .init( 67 | initialState: MainState(), 68 | reducer: mainReducer, 69 | environment: .init( 70 | cardsClient: .mockPreview(), 71 | favoriteCardsClient: .mockPreview(), 72 | mainQueue: DispatchQueue.main.eraseToAnyScheduler(), 73 | uuid: UUID.init 74 | ) 75 | ) 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /PokemonCards/Data/Client/CardsClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardsClient.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 27/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | 11 | struct CardsClient { 12 | var page: (_ number: Int, _ size: Int) -> Effect 13 | } 14 | 15 | // MARK: - Live 16 | 17 | extension CardsClient { 18 | static let live = CardsClient( 19 | page: { number, size in 20 | Provider.shared 21 | .cardsPage(number: number, size: size) 22 | .eraseToEffect() 23 | } 24 | ) 25 | } 26 | 27 | // MARK: - Mock 28 | 29 | extension CardsClient { 30 | static func mock( 31 | all: @escaping (Int, Int) -> Effect = { _, _ in 32 | fatalError("Unmocked") 33 | } 34 | ) -> Self { 35 | Self( 36 | page: all 37 | ) 38 | } 39 | 40 | static func mockPreview( 41 | all: @escaping (Int, Int) -> Effect = { _, _ in 42 | .init(value: Cards.mock) 43 | } 44 | ) -> Self { 45 | Self( 46 | page: all 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PokemonCards/Data/Client/FavoriteCardsClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteCardsClient.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 27/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | 11 | struct FavoriteCardsClient { 12 | var all: () -> Effect<[Card], Never> 13 | var add: (_ card: Card) -> Effect<[Card], Never> 14 | var remove: (_ card: Card) -> Effect<[Card], Never> 15 | } 16 | 17 | // MARK: - Live 18 | 19 | extension FavoriteCardsClient { 20 | static let live = FavoriteCardsClient( 21 | all: { 22 | Provider.shared 23 | .getFavorites() 24 | }, 25 | add: { card in 26 | Provider.shared 27 | .addFavorite(card) 28 | }, 29 | remove: { card in 30 | Provider.shared 31 | .removeFavorite(card) 32 | } 33 | ) 34 | } 35 | 36 | // MARK: - Mock 37 | 38 | extension FavoriteCardsClient { 39 | static func mock( 40 | all: @escaping () -> Effect<[Card], Never> = { 41 | fatalError("Unmocked") 42 | }, 43 | add: @escaping (Card) -> Effect<[Card], Never> = { _ in 44 | fatalError("Unmocked") 45 | }, 46 | remove: @escaping (Card) -> Effect<[Card], Never> = { _ in 47 | fatalError("Unmocked") 48 | } 49 | ) -> Self { 50 | Self( 51 | all: all, 52 | add: add, 53 | remove: remove 54 | ) 55 | } 56 | 57 | static func mockPreview( 58 | all: @escaping () -> Effect<[Card], Never> = { 59 | .init(value: [Card.mock1]) 60 | }, 61 | add: @escaping (Card) -> Effect<[Card], Never> = { _ in 62 | .init(value: [Card.mock2]) 63 | }, 64 | remove: @escaping (Card) -> Effect<[Card], Never> = { _ in 65 | .init(value: []) 66 | } 67 | ) -> Self { 68 | Self( 69 | all: all, 70 | add: add, 71 | remove: remove 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /PokemonCards/Data/Client/Provider/CoreData/CoreData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreData.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 11/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | 11 | class CoreData { 12 | static var shared = CoreData() 13 | 14 | var context: NSManagedObjectContext { 15 | return persistentContainer.viewContext 16 | } 17 | 18 | private let containerName = "PokemonCards" 19 | lazy var persistentContainer: NSPersistentContainer = { 20 | let container = NSPersistentContainer(name: containerName) 21 | container.loadPersistentStores(completionHandler: { _, error in 22 | if let error = error as NSError? { 23 | debugPrint("Unresolved error \(error), \(error.userInfo)") 24 | } 25 | }) 26 | return container 27 | }() 28 | 29 | public func saveContext() { 30 | let context = persistentContainer.viewContext 31 | if context.hasChanges { 32 | do { 33 | try context.save() 34 | } catch { 35 | let nserror = error as NSError 36 | debugPrint("Unresolved error \(nserror), \(nserror.userInfo)") 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /PokemonCards/Data/Client/Provider/CoreData/FavoriteCard+Utility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteCard+Utility.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 17/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | 11 | extension FavoriteCard { 12 | static func instance(from card: Card, with context: NSManagedObjectContext) -> FavoriteCard { 13 | let newFavorite = FavoriteCard(context: context) 14 | newFavorite.id = card.id 15 | newFavorite.name = card.name 16 | newFavorite.hp = card.hp 17 | newFavorite.imageURL = card.images.smallURLString 18 | newFavorite.imageHDURL = card.images.largeURLString 19 | 20 | return newFavorite 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /PokemonCards/Data/Client/Provider/CoreData/PokemonCards.xcdatamodeld/Cards.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /PokemonCards/Data/Client/Provider/Provider+Cards.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Provider+Cards.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 26/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | extension Provider { 13 | /** 14 | # TGC Reference 15 | https://docs.pokemontcg.io/#api_v1cards_list 16 | */ 17 | func cardsPage(number: Int, size: Int) -> AnyPublisher { 18 | var request = URLRequest(url: Router.cardsPage(number: number, size: size).url!) 19 | request.httpMethod = "GET" 20 | 21 | return requestAuthorizedPublisher(request) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PokemonCards/Data/Client/Provider/Provider+FavoriteCards.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Provider+FavoriteCards.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 11/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | import Foundation 11 | 12 | extension Provider { 13 | func getFavorites() -> Effect<[Card], Never> { 14 | let cards = getFavorites().map { Card(with: $0) } 15 | return .init(value: cards) 16 | } 17 | 18 | private func getFavorites() -> [FavoriteCard] { 19 | var favorites = [FavoriteCard]() 20 | do { 21 | favorites = try context.fetch(FavoriteCard.fetchRequest()) 22 | } catch { 23 | debugPrint("error retrieving cards: \(error)") 24 | } 25 | 26 | return favorites 27 | } 28 | 29 | func addFavorite(_ card: Card) -> Effect<[Card], Never> { 30 | guard !hasFavorite(with: card) else { return getFavorites() } 31 | 32 | _ = FavoriteCard.instance(from: card, with: context) 33 | CoreData.shared.saveContext() 34 | 35 | return getFavorites() 36 | } 37 | 38 | func removeFavorite(_ card: Card) -> Effect<[Card], Never> { 39 | guard 40 | let favoriteId = getFavorites().filter({ $0.id == card.id }).first?.objectID, 41 | let favoriteCard = context.object(with: favoriteId) as? FavoriteCard 42 | else { return getFavorites() } 43 | 44 | context.delete(favoriteCard) 45 | CoreData.shared.saveContext() 46 | 47 | return getFavorites() 48 | } 49 | 50 | private func hasFavorite(with card: Card) -> Bool { 51 | let favorite = getFavorites().filter { $0.id == card.id }.first 52 | return favorite != nil 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /PokemonCards/Data/Client/Provider/Provider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Provider.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 26/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | class Provider { 13 | static var shared = Provider() 14 | let context = CoreData.shared.context 15 | 16 | func requestPublisher(_ request: URLRequest) -> AnyPublisher { 17 | URLSession.shared.dataTaskPublisher(for: request) 18 | .mapError { .network(error: $0) } 19 | .flatMap { self.requestDecoder(data: $0.data) } 20 | .eraseToAnyPublisher() 21 | } 22 | 23 | func requestAuthorizedPublisher(_ request: URLRequest) -> AnyPublisher { 24 | var request = request 25 | let apiKey = Environment.apiKey 26 | request.setValue(apiKey, forHTTPHeaderField: "X-Api-Key") 27 | 28 | return requestPublisher(request) 29 | } 30 | } 31 | 32 | // MARK: - Encode/Decode Requests 33 | 34 | extension Provider { 35 | private func requestDecoder(data: Data) -> AnyPublisher { 36 | let decoder = JSONDecoder() 37 | decoder.keyDecodingStrategy = .convertFromSnakeCase 38 | 39 | return Just(data) 40 | .tryMap { try decoder.decode(T.self, from: $0) } 41 | .mapError { .decoding(error: $0) } 42 | .eraseToAnyPublisher() 43 | } 44 | 45 | func requestEncoder(data: T) -> AnyPublisher { 46 | return Just(data) 47 | .tryMap { try JSONEncoder().encode($0) } 48 | .mapError { .encoding(error: $0) } 49 | .eraseToAnyPublisher() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /PokemonCards/Data/Client/Provider/ProviderError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProviderError.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 27/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum ProviderError: Error, Equatable { 12 | static func == (lhs: ProviderError, rhs: ProviderError) -> Bool { 13 | switch (lhs, rhs) { 14 | case (.network(let lhsError), .network(let rhsError)): 15 | return ErrorUtility.areEqual(lhsError, rhsError) 16 | case (.decoding(let lhsError), .decoding(let rhsError)): 17 | return ErrorUtility.areEqual(lhsError, rhsError) 18 | case (.encoding(let lhsError), .encoding(let rhsError)): 19 | return ErrorUtility.areEqual(lhsError, rhsError) 20 | case (.error(let lhsError), .error(let rhsError)): 21 | return ErrorUtility.areEqual(lhsError, rhsError) 22 | default: return false 23 | } 24 | } 25 | 26 | case network(error: Error) 27 | case decoding(error: Error) 28 | case encoding(error: Error) 29 | case error(error: Error) 30 | case notFound 31 | } 32 | -------------------------------------------------------------------------------- /PokemonCards/Data/Client/Provider/Router/Router+Cards.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router+Cards.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 26/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Router { 12 | static func cardsPage(number: Int, size: Int) -> Route { 13 | return 14 | Route( 15 | path: "cards", 16 | queryItems: [ 17 | .init(name: "page", value: "\(number)"), 18 | .init(name: "pageSize", value: "\(size)") 19 | ] 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /PokemonCards/Data/Client/Provider/Router/Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 26/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Route { 12 | let path: String 13 | let queryItems: [URLQueryItem]? 14 | 15 | init(path: String, queryItems: [URLQueryItem]? = nil) { 16 | self.path = path 17 | self.queryItems = queryItems 18 | } 19 | 20 | var url: URL? { 21 | var components = URLComponents() 22 | 23 | components.scheme = "https" 24 | components.host = Environment.apiURL.absoluteString 25 | components.path = "/\(Environment.apiVersion)/\(path)" 26 | components.queryItems = queryItems 27 | 28 | return components.url 29 | } 30 | } 31 | 32 | struct Router {} 33 | -------------------------------------------------------------------------------- /PokemonCards/Data/Model/Cards.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cards.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 02/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Card: Codable, Equatable, Identifiable { 12 | var id: String 13 | var name: String 14 | var hp: String? 15 | 16 | var images: Images 17 | 18 | init( 19 | id: String, 20 | name: String, 21 | hp: String? = nil, 22 | imageURLString: String, 23 | imageHDURLString: String 24 | ) { 25 | self.id = id 26 | self.name = name 27 | self.hp = hp 28 | self.images = Images(smallURLString: imageURLString, 29 | largeURLString: imageHDURLString) 30 | } 31 | } 32 | 33 | struct Images: Codable, Equatable { 34 | var smallURLString: String 35 | var smallURL: URL? { 36 | URL(string: smallURLString) 37 | } 38 | 39 | var largeURLString: String 40 | var largeURL: URL? { 41 | URL(string: smallURLString) 42 | } 43 | 44 | enum CodingKeys: String, CodingKey { 45 | case smallURLString = "small" 46 | case largeURLString = "large" 47 | } 48 | } 49 | 50 | // MARK: Inits 51 | 52 | extension Card { 53 | init(with favorite: FavoriteCard) { 54 | self.id = favorite.id ?? "" 55 | self.name = favorite.name ?? "" 56 | self.hp = favorite.hp 57 | self.images = Images(smallURLString: favorite.imageURL ?? "", 58 | largeURLString: favorite.imageHDURL ?? "") 59 | } 60 | } 61 | 62 | // MARK: Mock 63 | 64 | extension Card { 65 | static var mock1: Card { 66 | Card( 67 | id: "xy7-4", 68 | name: "Bellossom", 69 | hp: "120", 70 | imageURLString: "https://images.pokemontcg.io/xy7/4.png", 71 | imageHDURLString: "https://images.pokemontcg.io/xy7/4_hires.png" 72 | ) 73 | } 74 | 75 | static var mock2: Card { 76 | Card( 77 | id: "ex16-1", 78 | name: "Aggron", 79 | hp: "110", 80 | imageURLString: "https://images.pokemontcg.io/ex16/1.png", 81 | imageHDURLString: "https://images.pokemontcg.io/ex16/1_hires.png" 82 | ) 83 | } 84 | } 85 | 86 | // MARK: - Cards 87 | 88 | struct Cards: Codable, Equatable, Identifiable { 89 | var id = UUID() 90 | var cards: [Card] 91 | 92 | enum CodingKeys: String, CodingKey { 93 | case cards = "data" 94 | } 95 | } 96 | 97 | // MARK: Mock 98 | 99 | extension Cards { 100 | static var mock: Cards = .init( 101 | cards: [ 102 | Card.mock1, 103 | Card.mock2 104 | ] 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /PokemonCards/Design System/Components/Buttons/FavoriteButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteButton.swift 3 | // Core 4 | // 5 | // Created by Daniel Almeida on 27/08/2020. 6 | // Copyright © 2020 Coletiv Studio, Lda. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct FavoriteButton: View { 12 | let action: () -> Void 13 | let isFavorite: Bool 14 | 15 | var body: some View { 16 | Button( 17 | action: action, 18 | label: { Image(systemName: isFavorite ? "star.fill" : "star") } 19 | ) 20 | .frame(width: 32, height: 44) 21 | } 22 | } 23 | 24 | // MARK: - Previews 25 | 26 | struct FavoriteButton_Previews: PreviewProvider { 27 | static var previews: some View { 28 | Group { 29 | FavoriteButton( 30 | action: {}, 31 | isFavorite: true 32 | ) 33 | .previewLayout(.sizeThatFits) 34 | FavoriteButton( 35 | action: {}, 36 | isFavorite: false 37 | ) 38 | .previewLayout(.sizeThatFits) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /PokemonCards/Design System/Components/Items/CardItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardItemView.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 02/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import KingfisherSwiftUI 10 | import SwiftUI 11 | 12 | struct CardItemView: View { 13 | let card: Card 14 | let isFavorite: Bool 15 | 16 | var body: some View { 17 | ZStack(alignment: .topTrailing) { 18 | VStack(alignment: .leading, spacing: 4) { 19 | KFImage(card.images.smallURL) 20 | .placeholder { 21 | ActivityIndicator( 22 | style: .large, 23 | isAnimating: .constant(true) 24 | ) 25 | } 26 | .resizable() 27 | .aspectRatio(CGSize(width: 600, height: 825), contentMode: .fit) 28 | .clipped() 29 | HStack { 30 | VStack(alignment: .leading, spacing: 0) { 31 | Text(card.name) 32 | .font(.system(size: 18, weight: .bold)) 33 | .lineLimit(1) 34 | Text(card.hp ?? "-") 35 | .font(.system(size: 12, weight: .regular)) 36 | } 37 | Spacer() 38 | if isFavorite { 39 | Image(systemName: "star.fill") 40 | .imageScale(.small) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | // MARK: - Previews 49 | 50 | struct CardItemView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | CardItemView(card: .mock1, isFavorite: true) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /PokemonCards/Design System/Representables/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicator.swift 3 | // Seya 4 | // 5 | // Created by Daniel Almeida on 02/06/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct ActivityIndicator: UIViewRepresentable { 12 | let style: UIActivityIndicatorView.Style 13 | let color: UIColor 14 | @Binding var isAnimating: Bool 15 | 16 | public init(style: UIActivityIndicatorView.Style, color: UIColor = .black, isAnimating: Binding) { 17 | self.style = style 18 | self.color = color 19 | _isAnimating = isAnimating 20 | } 21 | 22 | public func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView { 23 | let activityIndicator = UIActivityIndicatorView(style: style) 24 | activityIndicator.color = color 25 | return activityIndicator 26 | } 27 | 28 | public func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) { 29 | isAnimating ? uiView.startAnimating() : uiView.stopAnimating() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PokemonCards/Localization/Localization+Cards.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Localization+Cards.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 03/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Localization { 12 | enum Cards { 13 | public static var title: LocalizedStringKey { 14 | return "Cards" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PokemonCards/Localization/Localization+Favorites.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Localization+Favorites.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 17/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Localization { 12 | enum Favorites { 13 | public static var title: LocalizedStringKey { 14 | return "Favorites" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PokemonCards/Localization/Localization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Localization.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 03/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | public enum Localization {} 10 | -------------------------------------------------------------------------------- /PokemonCards/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /PokemonCards/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /PokemonCards/Resources/Configs/Production.xcconfig: -------------------------------------------------------------------------------- 1 | // Production.xcconfig 2 | 3 | // Server URL 4 | API_URL = api.pokemontcg.io 5 | API_VERSION = v2 6 | API_KEY = 7 | 8 | // App Settings 9 | APP_NAME = PokemonCards 10 | APP_BUNDLE_ID = coletiv.ios.template 11 | -------------------------------------------------------------------------------- /PokemonCards/Resources/Configs/Staging.xcconfig: -------------------------------------------------------------------------------- 1 | // Staging.xcconfig 2 | 3 | // Server URL 4 | API_URL = api.pokemontcg.io 5 | API_VERSION = v2 6 | API_KEY = 7 | 8 | // App Settings 9 | APP_NAME = PokemonCards Staging 10 | APP_BUNDLE_ID = coletiv.ios.template.staging 11 | -------------------------------------------------------------------------------- /PokemonCards/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_VERSION 6 | $(API_VERSION) 7 | API_URL 8 | $(API_URL) 9 | API_KEY 10 | $(API_KEY) 11 | CFBundleDevelopmentRegion 12 | $(DEVELOPMENT_LANGUAGE) 13 | CFBundleDisplayName 14 | ${APP_NAME} 15 | CFBundleExecutable 16 | $(EXECUTABLE_NAME) 17 | CFBundleIdentifier 18 | $(APP_BUNDLE_ID) 19 | CFBundleInfoDictionaryVersion 20 | 6.0 21 | CFBundleName 22 | $(APP_NAME) 23 | CFBundlePackageType 24 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 25 | CFBundleShortVersionString 26 | $(MARKETING_VERSION) 27 | CFBundleVersion 28 | 1 29 | LSRequiresIPhoneOS 30 | 31 | UIApplicationSceneManifest 32 | 33 | UIApplicationSupportsMultipleScenes 34 | 35 | UISceneConfigurations 36 | 37 | UIWindowSceneSessionRoleApplication 38 | 39 | 40 | UISceneConfigurationName 41 | Default Configuration 42 | UISceneDelegateClassName 43 | $(PRODUCT_MODULE_NAME).SceneDelegate 44 | 45 | 46 | 47 | 48 | UILaunchStoryboardName 49 | LaunchScreen 50 | UIRequiredDeviceCapabilities 51 | 52 | armv7 53 | 54 | UISupportedInterfaceOrientations 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationLandscapeLeft 58 | UIInterfaceOrientationLandscapeRight 59 | 60 | UISupportedInterfaceOrientations~ipad 61 | 62 | UIInterfaceOrientationPortrait 63 | UIInterfaceOrientationPortraitUpsideDown 64 | UIInterfaceOrientationLandscapeLeft 65 | UIInterfaceOrientationLandscapeRight 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /PokemonCards/Resources/Localization/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localization.strings 3 | PokemonCards 4 | 5 | Created by Daniel Almeida on 03/12/2020. 6 | Copyright © 2020 Coletiv. All rights reserved. 7 | */ 8 | 9 | "Cards" = "Cards"; 10 | "Filters" = "Filters"; 11 | -------------------------------------------------------------------------------- /PokemonCards/Resources/Localization/pt-PT.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localization.strings 3 | PokemonCards 4 | 5 | Created by Daniel Almeida on 03/12/2020. 6 | Copyright © 2020 Coletiv. All rights reserved. 7 | */ 8 | 9 | "Cards" = "Cartas"; 10 | "Filters" = "Filtros"; 11 | -------------------------------------------------------------------------------- /PokemonCards/Resources/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /PokemonCards/Utility/Environment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 26/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | # Inspired by: 13 | https://thoughtbot.com/blog/let-s-setup-your-ios-environments 14 | */ 15 | public enum Environment { 16 | // MARK: Keys 17 | 18 | enum ConfigKey { 19 | static let apiURL = "API_URL" 20 | static let apiVersion = "API_VERSION" 21 | static let apiKey = "API_KEY" 22 | } 23 | 24 | // MARK: Plist 25 | 26 | private static let infoDictionary: [String: Any] = { 27 | guard let dict = Bundle.main.infoDictionary else { 28 | fatalError("Plist file not found") 29 | } 30 | return dict 31 | }() 32 | 33 | // MARK: Plist values 34 | 35 | static let apiURL: URL = { 36 | guard let apiURL = Environment.infoDictionary[ConfigKey.apiURL] as? String else { 37 | fatalError("API URL not set in plist for this environment") 38 | } 39 | guard let url = URL(string: apiURL) else { 40 | fatalError("API URL is invalid") 41 | } 42 | return url 43 | }() 44 | 45 | static let apiVersion: String = { 46 | guard let apiVersion = Environment.infoDictionary[ConfigKey.apiVersion] as? String else { 47 | fatalError("API Version not set in plist for this environment") 48 | } 49 | 50 | return apiVersion 51 | }() 52 | 53 | static let apiKey: String = { 54 | guard let apiKey = Environment.infoDictionary[ConfigKey.apiKey] as? String else { 55 | fatalError("API Key not set in plist for this environment") 56 | } 57 | 58 | return apiKey 59 | }() 60 | } 61 | -------------------------------------------------------------------------------- /PokemonCards/Utility/Error+Equality.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error+Equality.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 27/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | # Inspired by: 13 | https://kandelvijaya.com/2018/04/21/blog_equalityonerror/ 14 | */ 15 | class ErrorUtility { 16 | public static func areEqual(_ lhs: Error, _ rhs: Error) -> Bool { 17 | return lhs.reflectedString == rhs.reflectedString 18 | } 19 | } 20 | 21 | public extension Error { 22 | var reflectedString: String { 23 | return String(reflecting: self) 24 | } 25 | 26 | func isEqual(to: Self) -> Bool { 27 | return reflectedString == to.reflectedString 28 | } 29 | } 30 | 31 | public extension NSError { 32 | func isEqual(to: NSError) -> Bool { 33 | let lhs = self as Error 34 | let rhs = to as Error 35 | return isEqual(to) && lhs.reflectedString == rhs.reflectedString 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /PokemonCards/Utility/UUID+Increment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UUID+Increment.swift 3 | // PokemonCards 4 | // 5 | // Created by Daniel Almeida on 11/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // swiftlint:disable line_length 12 | /** 13 | # Taken from Composable Architecture Examples: 14 | https://github.com/pointfreeco/swift-composable-architecture/blob/c1f88bd8608e8a26fd61ab46c9f11a63b0f4023d/Examples/CaseStudies/SwiftUICaseStudies/03-Effects-SystemEnvironment.swift#L216 15 | */ 16 | // swiftlint:enable line_length 17 | extension UUID { 18 | /// A deterministic, auto-incrementing "UUID" generator for testing. 19 | static var incrementing: () -> UUID { 20 | var uuid = 0 21 | return { 22 | defer { uuid += 1 } 23 | return UUID(uuidString: "00000000-0000-0000-0000-\(String(format: "%012x", uuid))")! 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PokemonCardsSnapshotTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /PokemonCardsSnapshotTests/PokemonCardsSnapshotTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PokemonCardsSnapshotTests.swift 3 | // PokemonCardsTests 4 | // 5 | // Created by Daniel Almeida on 16/12/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | import SnapshotTesting 11 | import SwiftUI 12 | @testable import PokemonCards 13 | import XCTest 14 | 15 | class PokemonCardsSnapshotTests: XCTestCase { 16 | func testCardsList() { 17 | let cardsClient: CardsClient = .mock( 18 | all: { [self] _, _ in 19 | let cards = cardDetailStates(12).map { $0.card } 20 | return .init(value: .init(cards: cards)) 21 | } 22 | ) 23 | 24 | let favoriteCardsClient: FavoriteCardsClient = .mock( 25 | all: { .init(value: [Card.mock2]) } 26 | ) 27 | 28 | let mainStore = Store( 29 | initialState: MainState(), 30 | reducer: mainReducer, 31 | environment: .init( 32 | cardsClient: cardsClient, 33 | favoriteCardsClient: favoriteCardsClient, 34 | mainQueue: DispatchQueue.main.eraseToAnyScheduler(), 35 | uuid: UUID.incrementing 36 | ) 37 | ) 38 | 39 | let mainView = MainView(store: mainStore) 40 | let vc = UIHostingController(rootView: mainView) 41 | vc.view.frame = UIScreen.main.bounds 42 | 43 | // isRecording = true 44 | 45 | ViewStore(mainStore.scope(state: { $0.cardsState })).send(.cards(.onAppear)) 46 | wait(0.5) 47 | assertSnapshot(matching: vc, as: .windowedImage) 48 | 49 | ViewStore(mainStore).send(.selectedTabChange(.favorites)) 50 | 51 | wait(0.5) 52 | assertSnapshot(matching: vc, as: .windowedImage) 53 | } 54 | 55 | /// NavigationLink has some problems when using a binding to activate the navigation, using this simple test to the view only 56 | func testCardDetail() { 57 | let store = Store( 58 | initialState: .init( 59 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, 60 | card: Card.mock1 61 | ), 62 | reducer: cardDetailReducer, 63 | environment: CardDetailEnvironment( 64 | favoriteCardsClient: .mockPreview(), 65 | mainQueue: DispatchQueue.main.eraseToAnyScheduler() 66 | ) 67 | ) 68 | 69 | let cardListView = CardDetailView(store: store) 70 | let vc = UIHostingController(rootView: cardListView) 71 | vc.view.frame = UIScreen.main.bounds 72 | 73 | // isRecording = true 74 | 75 | wait(0.5) 76 | assertSnapshot(matching: vc, as: .windowedImage) 77 | } 78 | 79 | // MARK: HELPER 80 | 81 | private func cardDetailStates(_ count: Int) -> [CardDetailState] { 82 | var mockStateCards: [CardDetailState] = [] 83 | for index in 1 ... count { 84 | mockStateCards.append( 85 | .init( 86 | id: UUID(uuidString: "00000000-0000-0000-0000-\(String(format: "%012x", index))")!, 87 | card: Card.mock1 88 | ) 89 | ) 90 | } 91 | return mockStateCards 92 | } 93 | 94 | private func wait(_ seconds: TimeInterval) { 95 | let expectation = self.expectation(description: "wait") 96 | DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { 97 | expectation.fulfill() 98 | } 99 | wait(for: [expectation], timeout: seconds * 2) 100 | } 101 | } 102 | 103 | /** 104 | # Taken from 105 | https://www.pointfree.co/episodes/ep86-swiftui-snapshot-testing 106 | */ 107 | extension Snapshotting where Value: UIViewController, Format == UIImage { 108 | static var windowedImage: Snapshotting { 109 | return Snapshotting.image.asyncPullback { vc in 110 | Async { callback in 111 | UIView.setAnimationsEnabled(false) 112 | let window = UIApplication.shared.windows.first! 113 | window.rootViewController = vc 114 | DispatchQueue.main.async { 115 | let image = UIGraphicsImageRenderer(bounds: window.bounds).image { _ in 116 | window.drawHierarchy(in: window.bounds, afterScreenUpdates: true) 117 | } 118 | callback(image) 119 | UIView.setAnimationsEnabled(true) 120 | } 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /PokemonCardsSnapshotTests/__Snapshots__/PokemonCardsSnapshotTests/testCardDetail.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/significa/ios-swiftui-tca-example/2ca4446393537f591e2bdb117d7d49dcf7b0bea1/PokemonCardsSnapshotTests/__Snapshots__/PokemonCardsSnapshotTests/testCardDetail.1.png -------------------------------------------------------------------------------- /PokemonCardsSnapshotTests/__Snapshots__/PokemonCardsSnapshotTests/testCardsList.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/significa/ios-swiftui-tca-example/2ca4446393537f591e2bdb117d7d49dcf7b0bea1/PokemonCardsSnapshotTests/__Snapshots__/PokemonCardsSnapshotTests/testCardsList.1.png -------------------------------------------------------------------------------- /PokemonCardsSnapshotTests/__Snapshots__/PokemonCardsSnapshotTests/testCardsList.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/significa/ios-swiftui-tca-example/2ca4446393537f591e2bdb117d7d49dcf7b0bea1/PokemonCardsSnapshotTests/__Snapshots__/PokemonCardsSnapshotTests/testCardsList.2.png -------------------------------------------------------------------------------- /PokemonCardsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /PokemonCardsTests/PokemonCardsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PokemonCardsTests.swift 3 | // PokemonCardsTests 4 | // 5 | // Created by Daniel Almeida on 26/11/2020. 6 | // Copyright © 2020 Coletiv. All rights reserved. 7 | // 8 | 9 | import ComposableArchitecture 10 | @testable import PokemonCards 11 | import XCTest 12 | 13 | class PokemonCardsTests: XCTestCase { 14 | static let scheduler = DispatchQueue.test 15 | 16 | struct TestError: Error {} 17 | let error: ProviderError = .network(error: TestError()) 18 | 19 | let mockStateCards: [CardDetailState] = [ 20 | .init( 21 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, 22 | card: Card.mock1 23 | ), 24 | .init( 25 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000001")!, 26 | card: Card.mock2 27 | ) 28 | ] 29 | 30 | let cardsStore = TestStore( 31 | initialState: CardsState(), 32 | reducer: cardsReducer, 33 | environment: CardsEnvironment( 34 | cardsClient: .mock(), 35 | favoriteCardsClient: .mock(), 36 | mainQueue: scheduler.eraseToAnyScheduler(), 37 | uuid: UUID.incrementing 38 | ) 39 | ) 40 | 41 | func testCardsListSuccess() { 42 | // Success retrieving data 43 | cardsStore.assert( 44 | .environment { 45 | $0.cardsClient.page = { _, _ in Effect(value: Cards.mock) } 46 | $0.favoriteCardsClient.all = { Effect(value: []) } 47 | }, 48 | 49 | .send(.retrieve) { _ in }, 50 | .receive(.loadingActive(true)) { 51 | $0.isLoading = true 52 | }, 53 | 54 | .do { PokemonCardsTests.scheduler.advance() }, 55 | .receive(.cardsResponse(.success(Cards.mock))) { 56 | $0.cards = .init(uniqueElements: self.mockStateCards) 57 | }, 58 | 59 | .receive(.loadingActive(false)) { 60 | $0.isLoading = false 61 | }, 62 | 63 | .receive(.loadingPageActive(false)) { 64 | $0.isLoadingPage = false 65 | }, 66 | 67 | .receive(.retrieveFavorites) { _ in }, 68 | 69 | .receive(.favoritesResponse(.success([]))) { 70 | $0.favorites = [] 71 | } 72 | ) 73 | } 74 | 75 | func testCardsListError() { 76 | // Error retrieving data 77 | cardsStore.assert( 78 | .environment { 79 | $0.cardsClient.page = { [self] _, _ in 80 | .init(error: error) 81 | } 82 | $0.favoriteCardsClient.all = { Effect(value: []) } 83 | }, 84 | 85 | .send(.retrieve) { _ in }, 86 | .receive(.loadingActive(true)) { 87 | $0.isLoading = true 88 | }, 89 | 90 | .do { PokemonCardsTests.scheduler.advance() }, 91 | .receive(.cardsResponse(.failure(error))) { 92 | $0.cards = [] 93 | }, 94 | 95 | .receive(.loadingActive(false)) { 96 | $0.isLoading = false 97 | }, 98 | 99 | .receive(.loadingPageActive(false)) { 100 | $0.isLoadingPage = false 101 | }, 102 | 103 | .receive(.retrieveFavorites) { _ in }, 104 | 105 | .receive(.favoritesResponse(.success([]))) { 106 | $0.favorites = [] 107 | } 108 | ) 109 | } 110 | 111 | func testToggleFavorite() { 112 | let store = TestStore( 113 | initialState: CardDetailState( 114 | id: .init(), 115 | card: .mock1 116 | ), 117 | reducer: cardDetailReducer, 118 | environment: CardDetailEnvironment( 119 | favoriteCardsClient: .mock(), 120 | mainQueue: PokemonCardsTests.scheduler.eraseToAnyScheduler() 121 | ) 122 | ) 123 | 124 | // Toggle Favorite 125 | store.assert( 126 | .environment { 127 | $0.favoriteCardsClient.all = { .init(value: []) } 128 | $0.favoriteCardsClient.add = { _ in .init(value: [Card.mock1]) } 129 | $0.favoriteCardsClient.remove = { _ in .init(value: []) } 130 | }, 131 | 132 | .send(.onAppear) { _ in }, 133 | .do { PokemonCardsTests.scheduler.advance() }, 134 | .receive(.favoritesResponse(.success([]))) { 135 | $0.favorites = [] 136 | }, 137 | 138 | .send(.toggleFavorite) { _ in }, 139 | .do { PokemonCardsTests.scheduler.advance() }, 140 | .receive(.toggleFavoriteResponse(.success([Card.mock1]))) { 141 | $0.favorites = [Card.mock1] 142 | $0.isFavorite = true 143 | }, 144 | 145 | .send(.toggleFavorite) { _ in }, 146 | .do { PokemonCardsTests.scheduler.advance() }, 147 | .receive(.toggleFavoriteResponse(.success([]))) { 148 | $0.favorites = [] 149 | $0.isFavorite = false 150 | } 151 | ) 152 | } 153 | 154 | func testFavoritesList() { 155 | let store = TestStore( 156 | initialState: FavoritesState(), 157 | reducer: favoritesReducer, 158 | environment: FavoritesEnvironment( 159 | favoriteCardsClient: .mock(), 160 | mainQueue: PokemonCardsTests.scheduler.eraseToAnyScheduler(), 161 | uuid: UUID.incrementing 162 | ) 163 | ) 164 | 165 | store.assert( 166 | .environment { 167 | $0.favoriteCardsClient.all = { Effect(value: Cards.mock.cards) } 168 | }, 169 | 170 | .send(.onAppear) { _ in }, 171 | .receive(.retrieveFavorites) { _ in }, 172 | 173 | .do { PokemonCardsTests.scheduler.advance() }, 174 | .receive(.favoritesResponse(.success(Cards.mock.cards))) { 175 | $0.cards = .init(uniqueElements: self.mockStateCards) 176 | } 177 | ) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PokemonCards - SwiftUI + Composable Architecture Example 2 | 3 | This repository has a small example using the [swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture) and the [Pokemon TGC API](https://pokemontcg.io). This also inspired [this](https://github.com/pointfreeco/swift-composable-architecture) Coletiv Blog post 4 | 5 | ## 🚧 Dependencies 6 | 7 | - [swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture) (`~> 0.9.0`) 8 | - [Kingfisher](https://github.com/onevcat/Kingfisher) (`~> 5.15.8`) 9 | - [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) (`~> 1.8.2`) 10 | 11 | ## 🏎 Kickstart 12 | 13 | ### Initial setup 14 | 15 | 1. Install [SwiftLint](https://github.com/realm/SwiftLint) `brew install swiftlint` 16 | 2. Install [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) pre-commit hook following [this](https://github.com/nicklockwood/SwiftFormat#git-pre-commit-hook) 17 | - If you are using the SourceTree client please also follow [this link](https://github.com/typicode/husky/issues/390#issuecomment-577008221) 18 | 3. Use the `.xcodeproj` file to open the Xcode project 19 | 4. Go to the `.xconfig` files under `PokemonCards/Resources/Configs` and insert your API Key on `API_KEY` var. You can get yours from the [Pokemon TGC](https://dev.pokemontcg.io/) website. -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | # app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app 2 | # apple_id("[[APPLE_ID]]") # Your Apple email address 3 | 4 | 5 | # For more information about the Appfile, see: 6 | # https://docs.fastlane.tools/advanced/#appfile 7 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:ios) 17 | 18 | platform :ios do 19 | desc "Test Staging" 20 | lane :test_staging do 21 | scan( 22 | project: "PokemonCards.xcodeproj", 23 | devices: ["iPhone 13"], 24 | scheme: "Staging", 25 | only_testing: "PokemonCardsTests" 26 | ) 27 | end 28 | 29 | desc "Test Production" 30 | lane :test_production do 31 | scan( 32 | project: "PokemonCards.xcodeproj", 33 | devices: ["iPhone 13"], 34 | scheme: "Production", 35 | only_testing: "PokemonCardsTests" 36 | ) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## iOS 17 | 18 | ### ios test_staging 19 | 20 | ```sh 21 | [bundle exec] fastlane ios test_staging 22 | ``` 23 | 24 | Test Staging 25 | 26 | ### ios test_production 27 | 28 | ```sh 29 | [bundle exec] fastlane ios test_production 30 | ``` 31 | 32 | Test Production 33 | 34 | ---- 35 | 36 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 37 | 38 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 39 | 40 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 41 | -------------------------------------------------------------------------------- /swiftformat: -------------------------------------------------------------------------------- 1 | --allman false 2 | --beforemarks 3 | --binarygrouping none 4 | --categorymark "MARK: %c" 5 | --classthreshold 0 6 | --closingparen balanced 7 | --commas inline 8 | --conflictmarkers reject 9 | --decimalgrouping none 10 | --elseposition same-line 11 | --enumthreshold 0 12 | --exponentcase lowercase 13 | --exponentgrouping disabled 14 | --fractiongrouping disabled 15 | --fragment false 16 | --funcattributes preserve 17 | --guardelse auto 18 | --header ignore 19 | --hexgrouping none 20 | --hexliteralcase uppercase 21 | --ifdef indent 22 | --importgrouping alphabetized 23 | --indent 2 24 | --indentcase false 25 | --lifecycle 26 | --linebreaks lf 27 | --maxwidth none 28 | --modifierorder 29 | --nospaceoperators 30 | --nowrapoperators 31 | --octalgrouping none 32 | --operatorfunc spaced 33 | --patternlet inline 34 | --ranges spaced 35 | --self init-only 36 | --semicolons never 37 | --shortoptionals always 38 | --smarttabs enabled 39 | --stripunusedargs closure-only 40 | --structthreshold 0 41 | --tabwidth unspecified 42 | --trailingclosures 43 | --trimwhitespace always 44 | --typeattributes preserve 45 | --varattributes preserve 46 | --voidtype void 47 | --wraparguments preserve 48 | --wrapcollections before-first 49 | --wrapparameters before-first 50 | --xcodeindentation disabled 51 | --yodaswap always 52 | --disable blankLinesBetweenScopes 53 | --disable wrapMultilineStatementBraces 54 | --------------------------------------------------------------------------------