├── .gitignore
├── .package.resolved
├── .swiftlint.yml
├── .tuist-version
├── AppArchitecture.png
├── AppTargets.drawio.png
├── Features
├── Backpack
│ ├── Example
│ │ ├── Resources
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── AppIcon.appiconset
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ ├── PokeBallLogo_1024.png
│ │ │ │ │ ├── PokeBallLogo_120.png
│ │ │ │ │ └── PokeBallLogo_180.png
│ │ │ │ ├── Backpack.imageset
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ └── PokemonTrainerBackpack.png
│ │ │ │ ├── Ball.imageset
│ │ │ │ │ ├── Ball.png
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── Contents.json
│ │ │ │ └── PokemonPlaceholder.imageset
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ └── PokemonPlaceholder.png
│ │ │ └── Base.lproj
│ │ │ │ └── LaunchScreen.storyboard
│ │ └── Sources
│ │ │ ├── AppController.swift
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Coordinator.swift
│ │ │ ├── MockDataFactory.swift
│ │ │ └── Scenes
│ │ │ └── Home Scene
│ │ │ ├── HomeActions.swift
│ │ │ ├── HomeDataProvider.swift
│ │ │ ├── HomePresenter.swift
│ │ │ ├── HomeViewController.storyboard
│ │ │ ├── HomeViewController.swift
│ │ │ └── HomeWireframe.swift
│ ├── Resources
│ │ └── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ └── PokemonPlaceholder.imageset
│ │ │ ├── Contents.json
│ │ │ └── PokemonPlaceholder.png
│ ├── Sources
│ │ └── Scenes
│ │ │ └── Backpack Scene
│ │ │ ├── BackpackActions.swift
│ │ │ ├── BackpackDataProvider.swift
│ │ │ ├── BackpackDataSource.swift
│ │ │ ├── BackpackDelegate.swift
│ │ │ ├── BackpackPresenter.swift
│ │ │ ├── BackpackViewController.storyboard
│ │ │ ├── BackpackViewController.swift
│ │ │ ├── BackpackWireframe.swift
│ │ │ └── Cells
│ │ │ ├── PokemonCollectionViewCell.swift
│ │ │ └── PokemonCollectionViewCell.xib
│ └── Tests
│ │ └── BackpackTests.swift
├── Catch
│ ├── Example
│ │ ├── Resources
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── AppIcon.appiconset
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ ├── PokeBallLogo_1024.png
│ │ │ │ │ ├── PokeBallLogo_120.png
│ │ │ │ │ └── PokeBallLogo_180.png
│ │ │ │ ├── Ball.imageset
│ │ │ │ │ ├── Ball.png
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── Contents.json
│ │ │ │ └── PokemonPlaceholder.imageset
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ └── PokemonPlaceholder.png
│ │ │ └── Base.lproj
│ │ │ │ └── LaunchScreen.storyboard
│ │ ├── Sources
│ │ │ ├── AppController.swift
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Coordinator.swift
│ │ │ ├── DataProviderExtension.swift
│ │ │ ├── Log.swift
│ │ │ └── Scenes
│ │ │ │ └── Home Scene
│ │ │ │ ├── HomeActions.swift
│ │ │ │ ├── HomeDataProvider.swift
│ │ │ │ ├── HomePresenter.swift
│ │ │ │ ├── HomeViewController.storyboard
│ │ │ │ ├── HomeViewController.swift
│ │ │ │ └── HomeWireframe.swift
│ │ └── Tests
│ │ │ └── AppTests.swift
│ ├── Resources
│ │ └── Assets.xcassets
│ │ │ ├── Background.imageset
│ │ │ ├── Contents.json
│ │ │ └── pokemonBackground.png
│ │ │ ├── Ball.imageset
│ │ │ ├── Ball.png
│ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ └── PokemonPlaceholder.imageset
│ │ │ ├── Contents.json
│ │ │ └── PokemonPlaceholder.png
│ ├── Sources
│ │ └── Scenes
│ │ │ └── Catch Scene
│ │ │ ├── CatchActions.swift
│ │ │ ├── CatchDataProvider.swift
│ │ │ ├── CatchPresenter.swift
│ │ │ ├── CatchViewController.storyboard
│ │ │ ├── CatchViewController.swift
│ │ │ └── CatchWireframe.swift
│ └── Tests
│ │ └── CatchUITests.swift
├── Common
│ ├── Sources
│ │ ├── Core
│ │ │ ├── Actions.swift
│ │ │ ├── AppData.swift
│ │ │ ├── Configuration.swift
│ │ │ ├── Constants.swift
│ │ │ ├── Coordinating.swift
│ │ │ ├── DataProvider.swift
│ │ │ ├── DataProviding.swift
│ │ │ ├── Notifier.swift
│ │ │ └── Updatable.swift
│ │ ├── Extensions
│ │ │ ├── Loadable.swift
│ │ │ ├── UIBarButtonItem+Extension.swift
│ │ │ ├── UITableViewCell+Extension.swift
│ │ │ └── UIViewController+StoryboardInstantiable.swift
│ │ ├── Model
│ │ │ ├── Generator.swift
│ │ │ ├── LocalPokemon.swift
│ │ │ ├── Pokemon.swift
│ │ │ ├── PokemonParser.swift
│ │ │ └── ScreenPokemon.swift
│ │ ├── Util
│ │ │ ├── FileStorage.swift
│ │ │ └── Storage.swift
│ │ └── Views
│ │ │ ├── PokemonView.swift
│ │ │ └── PokemonView.xib
│ └── Tests
│ │ └── CommonTests.swift
├── Detail
│ ├── Example
│ │ ├── Resources
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── AppIcon.appiconset
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ ├── PokeBallLogo_1024.png
│ │ │ │ │ ├── PokeBallLogo_120.png
│ │ │ │ │ └── PokeBallLogo_180.png
│ │ │ │ ├── Ball.imageset
│ │ │ │ │ ├── Ball.png
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ └── LaunchScreen.storyboard
│ │ └── Sources
│ │ │ ├── AppController.swift
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Coordinator.swift
│ │ │ └── MockDataFactory.swift
│ ├── Sources
│ │ └── Scenes
│ │ │ ├── DetailViewController.swift
│ │ │ └── Pokemon Detail Scene
│ │ │ ├── PokemonDetailPresenter.swift
│ │ │ ├── PokemonDetailViewController.storyboard
│ │ │ ├── PokemonDetailViewController.swift
│ │ │ └── PokemonDetailWireframe.swift
│ └── Tests
│ │ └── DetailTests.swift
├── Haneke
│ ├── Example
│ │ ├── Resources
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── AppIcon.appiconset
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ ├── PokeBallLogo_1024.png
│ │ │ │ │ ├── PokeBallLogo_120.png
│ │ │ │ │ └── PokeBallLogo_180.png
│ │ │ │ ├── Ball.imageset
│ │ │ │ │ ├── Ball.png
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ └── Base.lproj
│ │ │ │ └── LaunchScreen.storyboard
│ │ └── Sources
│ │ │ └── AppDelegate.swift
│ ├── Sources
│ │ ├── HNKCache.h
│ │ ├── HNKCache.m
│ │ ├── HNKDiskCache.h
│ │ ├── HNKDiskCache.m
│ │ ├── HNKDiskFetcher.h
│ │ ├── HNKDiskFetcher.m
│ │ ├── HNKNetworkFetcher.h
│ │ ├── HNKNetworkFetcher.m
│ │ ├── HNKSimpleFetcher.h
│ │ ├── HNKSimpleFetcher.m
│ │ ├── Haneke.h
│ │ ├── UIButton+Haneke.h
│ │ ├── UIButton+Haneke.m
│ │ ├── UIImageView+Haneke.h
│ │ ├── UIImageView+Haneke.m
│ │ ├── UIView+Haneke.h
│ │ └── UIView+Haneke.m
│ └── Tests
│ │ └── HanekeKitTests.swift
├── Home
│ ├── Example
│ │ ├── Resources
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── AppIcon.appiconset
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ ├── PokeBallLogo_1024.png
│ │ │ │ │ ├── PokeBallLogo_120.png
│ │ │ │ │ └── PokeBallLogo_180.png
│ │ │ │ ├── Ball.imageset
│ │ │ │ │ ├── Ball.png
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ └── Base.lproj
│ │ │ │ └── LaunchScreen.storyboard
│ │ └── Sources
│ │ │ ├── AppController.swift
│ │ │ ├── AppDelegate.swift
│ │ │ └── Coordinator.swift
│ ├── Resources
│ │ └── Assets.xcassets
│ │ │ ├── Backpack.imageset
│ │ │ ├── Contents.json
│ │ │ └── PokemonTrainerBackpack.png
│ │ │ ├── Ball.imageset
│ │ │ ├── Ball.png
│ │ │ └── Contents.json
│ │ │ └── Contents.json
│ ├── Sources
│ │ └── Scenes
│ │ │ └── Home Scene
│ │ │ ├── HomeActions.swift
│ │ │ ├── HomeDataProvider.swift
│ │ │ ├── HomePresenter.swift
│ │ │ ├── HomeViewController.storyboard
│ │ │ ├── HomeViewController.swift
│ │ │ └── HomeWireframe.swift
│ └── Tests
│ │ └── HomeUITests.swift
├── Network
│ ├── Example
│ │ ├── Resources
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── AppIcon.appiconset
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ ├── PokeBallLogo_1024.png
│ │ │ │ │ ├── PokeBallLogo_120.png
│ │ │ │ │ └── PokeBallLogo_180.png
│ │ │ │ ├── Ball.imageset
│ │ │ │ │ ├── Ball.png
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ └── Base.lproj
│ │ │ │ └── LaunchScreen.storyboard
│ │ └── Sources
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Constants.swift
│ │ │ ├── DataProviderExtension.swift
│ │ │ ├── Log.swift
│ │ │ └── SimpleViewController.swift
│ ├── Resources
│ │ └── Pokemon5.json
│ ├── Sources
│ │ ├── Core
│ │ │ ├── Configuration.swift
│ │ │ └── Constants.swift
│ │ ├── Mock
│ │ │ ├── MockData.swift
│ │ │ └── Pokemon5.json
│ │ └── Services
│ │ │ ├── PokemonSearchEndpoint+FactoryMethods.swift
│ │ │ ├── PokemonSearchEndpoint.swift
│ │ │ └── PokemonSearchService.swift
│ └── Tests
│ │ ├── Mock
│ │ ├── MockData.swift
│ │ └── Pokemon5.json
│ │ ├── MockSessionFactory.swift
│ │ ├── NetworkKitTests.swift
│ │ └── URLProtocolMock.swift
└── Pokedex
│ ├── .tuist-version
│ ├── Resources
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── PokeBallLogo_1024.png
│ │ │ ├── PokeBallLogo_120.png
│ │ │ └── PokeBallLogo_180.png
│ │ ├── Ball.imageset
│ │ │ ├── Ball.png
│ │ │ └── Contents.json
│ │ └── Contents.json
│ └── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── Sources
│ └── Core
│ │ ├── AppController.swift
│ │ ├── AppDelegate.swift
│ │ ├── Coordinator.swift
│ │ ├── DataProviderExtension.swift
│ │ └── Log.swift
│ ├── Tests
│ ├── AppDataTests.swift
│ ├── GeneratorTests.swift
│ ├── Mocks
│ │ ├── MockData.swift
│ │ ├── Pokemon12.json
│ │ ├── Pokemon12.png
│ │ ├── Pokemon5.json
│ │ └── Pokemon5.png
│ └── PokemonParserTests.swift
│ └── UITests
│ ├── PokedexAsyncSearchUITests.swift
│ ├── PokedexUITests.swift
│ └── Server_401_Error_UITest.swift
├── Gemfile
├── LICENSE
├── ModuleTargets.drawio.png
├── PokedexSchemes.png
├── PokedexScreens.png
├── Project.swift
├── README.md
├── Tuist
├── Config.swift
├── Dependencies.swift
├── ProjectDescriptionHelpers
│ └── Project+Templates.swift
└── Templates
│ ├── framework
│ ├── Framework.stencil
│ ├── UnitTests.stencil
│ └── framework.swift
│ └── module
│ ├── ExampleAppController.stencil
│ ├── ExampleAppDelegate.stencil
│ ├── ExampleCoordinator.stencil
│ ├── LaunchScreen.stencil
│ ├── Module.swift
│ ├── Resources
│ └── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── PokeBallLogo_1024.png
│ │ ├── PokeBallLogo_120.png
│ │ └── PokeBallLogo_180.png
│ │ ├── Ball.imageset
│ │ ├── Ball.png
│ │ └── Contents.json
│ │ └── Contents.json
│ ├── Scene.stencil
│ └── Tests.stencil
├── graph.png
└── scripts
└── swiftlint.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata/
15 | *.xccheckout
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 |
22 | # Bundler
23 | .bundle
24 |
25 | Carthage
26 | # We recommend against adding the Pods directory to your .gitignore. However
27 | # you should judge for yourself, the pros and cons are mentioned at:
28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
29 | #
30 | # Note: if you ignore the Pods directory, make sure to uncomment
31 | # `pod install` in .travis.yml
32 | #
33 | Pods/
34 |
35 | ### Projects ###
36 | *.xcodeproj
37 | *.xcworkspace
38 |
39 | ### Tuist derived files ###
40 | graph.dot
41 | Derived/
42 | Tuist/Dependencies/
43 |
--------------------------------------------------------------------------------
/.package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "JGProgressHUD",
6 | "repositoryURL": "https://github.com/JonasGessner/JGProgressHUD",
7 | "state": {
8 | "branch": null,
9 | "revision": "78d7cd35f1d90ff74fd82e486f2cbe4b24be8cf9",
10 | "version": "2.2.0"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - line_length
3 | - type_name
4 | - vertical_parameter_alignment
5 | - trailing_whitespace
6 |
7 | opt_in_rules:
8 | - empty_count
9 | - force_unwrapping
10 |
11 | excluded:
12 | - ./Pods
13 |
14 | function_body_length:
15 | warning: 50
16 |
17 |
--------------------------------------------------------------------------------
/.tuist-version:
--------------------------------------------------------------------------------
1 | 2.6.0
--------------------------------------------------------------------------------
/AppArchitecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/AppArchitecture.png
--------------------------------------------------------------------------------
/AppTargets.drawio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/AppTargets.drawio.png
--------------------------------------------------------------------------------
/Features/Backpack/Example/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 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "PokeBallLogo_120.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "PokeBallLogo_180.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "1024x1024",
47 | "idiom" : "ios-marketing",
48 | "filename" : "PokeBallLogo_1024.png",
49 | "scale" : "1x"
50 | }
51 | ],
52 | "info" : {
53 | "version" : 1,
54 | "author" : "xcode"
55 | }
56 | }
--------------------------------------------------------------------------------
/Features/Backpack/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Backpack/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png
--------------------------------------------------------------------------------
/Features/Backpack/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Backpack/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png
--------------------------------------------------------------------------------
/Features/Backpack/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Backpack/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png
--------------------------------------------------------------------------------
/Features/Backpack/Example/Resources/Assets.xcassets/Backpack.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "PokemonTrainerBackpack.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Backpack/Example/Resources/Assets.xcassets/Backpack.imageset/PokemonTrainerBackpack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Backpack/Example/Resources/Assets.xcassets/Backpack.imageset/PokemonTrainerBackpack.png
--------------------------------------------------------------------------------
/Features/Backpack/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Backpack/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png
--------------------------------------------------------------------------------
/Features/Backpack/Example/Resources/Assets.xcassets/Ball.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Ball.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Backpack/Example/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Features/Backpack/Example/Resources/Assets.xcassets/PokemonPlaceholder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "PokemonPlaceholder.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Backpack/Example/Resources/Assets.xcassets/PokemonPlaceholder.imageset/PokemonPlaceholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Backpack/Example/Resources/Assets.xcassets/PokemonPlaceholder.imageset/PokemonPlaceholder.png
--------------------------------------------------------------------------------
/Features/Backpack/Example/Sources/AppController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppController.swift
3 | // Wefox Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | protocol AppControlling {
13 | func start()
14 | }
15 |
16 | class AppController: AppControlling {
17 | var coordinator: Coordinating?
18 |
19 | func start() {
20 | let dataProvider = DataProvider()
21 |
22 | dataProvider.start()
23 |
24 | coordinator = Coordinator()
25 | coordinator?.dataProvider = dataProvider
26 | coordinator?.start()
27 |
28 | dataProvider.appData.pokemons = MockDataFactory.makePokemons()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Features/Backpack/Example/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Common
3 |
4 | @main
5 | class AppDelegate: UIResponder, UIApplicationDelegate {
6 |
7 | private let appController = AppController()
8 |
9 | func application(
10 | _ application: UIApplication,
11 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
12 | ) -> Bool {
13 |
14 | appController.start()
15 |
16 | return true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Features/Backpack/Example/Sources/Coordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Coordinator.swift
3 | // Wefox Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import Common
12 | import BackpackUI
13 | import Detail
14 |
15 | class Coordinator: Coordinating {
16 | let window: UIWindow
17 | var dataProvider: DataProvider?
18 | lazy var actions = Actions(coordinator: self)
19 | var currentViewController: UIViewController?
20 |
21 | init() {
22 | window = UIWindow(frame: UIScreen.main.bounds)
23 | window.makeKeyAndVisible()
24 | }
25 |
26 | func start() {
27 | actions.dataProvider = dataProvider
28 |
29 | showHomeScene()
30 | }
31 |
32 | func showHomeScene() {
33 | guard let dataProvider = dataProvider else { return }
34 | let viewController = HomeWireframe.makeViewController()
35 | HomeWireframe.prepare(viewController, actions: actions as HomeActions, dataProvider: dataProvider as HomeDataProvider)
36 |
37 | window.rootViewController = viewController
38 | }
39 |
40 | func showBackpackScene() {
41 | guard let dataProvider = dataProvider else { return }
42 |
43 | let navigationController = BackpackWireframe.makeNavigationController()
44 | guard let viewController = navigationController.topViewController as? BackpackViewController else { return }
45 |
46 | BackpackWireframe.prepare(viewController,
47 | actions: actions as BackpackActions,
48 | dataProvider: dataProvider as BackpackDataProvider)
49 |
50 | guard let topViewController = window.rootViewController else { return }
51 |
52 | topViewController.present(navigationController, animated: true, completion: nil)
53 |
54 | currentViewController = navigationController
55 | }
56 |
57 | func showPokemonDetailScene(pokemon: LocalPokemon) {
58 | let viewController = PokemonDetailWireframe.makeViewController()
59 |
60 | PokemonDetailWireframe.prepare(viewController, pokemon: pokemon)
61 |
62 | guard let topViewController = currentViewController as? UINavigationController else { return }
63 |
64 | topViewController.pushViewController(viewController, animated: true)
65 | }
66 |
67 | func showAlert(with message: String) {
68 | let alertController = UIAlertController(title: nil,
69 | message: message,
70 | preferredStyle: .alert)
71 |
72 | let okButton = UIAlertAction(title: Constants.Translations.ok,
73 | style: .default,
74 | handler: nil)
75 |
76 | alertController.addAction(okButton)
77 |
78 | guard let viewController = currentViewController else { return }
79 |
80 | viewController.present(alertController,
81 | animated: true,
82 | completion: nil)
83 | }
84 |
85 | func showLoading() {
86 |
87 | }
88 |
89 | func dismissLoading() {
90 |
91 | }
92 |
93 | func showCatchScene() {
94 |
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Features/Backpack/Example/Sources/MockDataFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockDataFactory.swift
3 | // Backpack
4 | //
5 | // Created by Ronan O Ciosig on 19/6/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | struct MockDataFactory {
13 | static func makePokemons() -> [LocalPokemon] {
14 |
15 | let pokemon1 = LocalPokemon(name: "cascoon",
16 | weight: 115,
17 | height: 7,
18 | order: 350,
19 | spriteUrlString: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/268.png",
20 | date: Date(),
21 | species: "cascoon",
22 | baseExperience: 72,
23 | types: ["bug"])
24 | let pokemon2 = LocalPokemon(name: "cranidos",
25 | weight: 315,
26 | height: 9,
27 | order: 519,
28 | spriteUrlString: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/408.png",
29 | date: Date(),
30 | species: "cranidos",
31 | baseExperience: 70,
32 | types: ["rock"])
33 |
34 | return [pokemon1, pokemon2]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Features/Backpack/Example/Sources/Scenes/Home Scene/HomeActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewActions.swift
3 | // Backpack
4 | //
5 | // Created by Ronan O Ciosig on 15/6/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | protocol HomeActions {
13 | func backpackButtonAction()
14 | }
15 |
16 | extension Actions: HomeActions {
17 | func backpackButtonAction() {
18 | coordinator.showBackpackScene()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Features/Backpack/Example/Sources/Scenes/Home Scene/HomeDataProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeDataProvider.swift
3 | // Backpack
4 | //
5 | // Created by Ronan O Ciosig on 15/6/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Common
10 |
11 | protocol HomeDataProvider {
12 |
13 | }
14 |
15 | extension DataProvider: HomeDataProvider {
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Features/Backpack/Example/Sources/Scenes/Home Scene/HomePresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomePresenter.swift
3 | // Backpack
4 | //
5 | // Created by Ronan O Ciosig on 15/6/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol HomeView: AnyObject {
12 |
13 | }
14 |
15 | protocol HomePresenting: AnyObject {
16 | func backpackButtonAction()
17 | }
18 |
19 | class HomePresenter: HomePresenting {
20 |
21 | // MARK: Properties
22 |
23 | private weak var view: HomeView?
24 | private var actions: HomeActions
25 | private var dataProvider: HomeDataProvider
26 |
27 | // MARK: Typealias
28 |
29 | typealias Actions = HomeActions
30 | typealias DataProvider = HomeDataProvider
31 | typealias View = HomeView
32 |
33 | required init(view: HomeView, actions: HomeActions, dataProvider: HomeDataProvider) {
34 | self.view = view
35 | self.actions = actions
36 | self.dataProvider = dataProvider
37 | }
38 |
39 | func backpackButtonAction() {
40 | actions.backpackButtonAction()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Features/Backpack/Example/Sources/Scenes/Home Scene/HomeViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Features/Backpack/Example/Sources/Scenes/Home Scene/HomeViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewController.swift
3 | // Backpack
4 | //
5 | // Created by Ronan O Ciosig on 15/6/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class HomeViewController: UIViewController {
12 | var presenter: HomePresenting?
13 |
14 | @IBAction func backpackButtonAction() {
15 | guard let presenter = presenter else { return }
16 | presenter.backpackButtonAction()
17 | }
18 | }
19 |
20 | extension HomeViewController: HomeView {
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/Features/Backpack/Example/Sources/Scenes/Home Scene/HomeWireframe.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeWireframe.swift
3 | // Backpack
4 | //
5 | // Created by Ronan O Ciosig on 15/6/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class HomeWireframe {
12 |
13 | static func makeViewController() -> HomeViewController {
14 | let storyboard = UIStoryboard.init(name: "HomeViewController", bundle: nil)
15 | return HomeViewController.instantiateFromStoryboard(storyboard: storyboard)
16 | }
17 |
18 | static func prepare(_ viewController: HomeViewController, actions: HomeActions, dataProvider: HomeDataProvider) {
19 | let presenter = HomePresenter(view: viewController, actions: actions, dataProvider: dataProvider)
20 | viewController.presenter = presenter
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/Features/Backpack/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Features/Backpack/Resources/Assets.xcassets/PokemonPlaceholder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "PokemonPlaceholder.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Backpack/Resources/Assets.xcassets/PokemonPlaceholder.imageset/PokemonPlaceholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Backpack/Resources/Assets.xcassets/PokemonPlaceholder.imageset/PokemonPlaceholder.png
--------------------------------------------------------------------------------
/Features/Backpack/Sources/Scenes/Backpack Scene/BackpackActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackpackActions.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Common
10 |
11 | public protocol BackpackActions {
12 | func selectItem(at index: Int)
13 | }
14 |
15 | extension Actions: BackpackActions {
16 | public func selectItem(at index: Int) {
17 | guard let dataProvider = dataProvider else { return }
18 | let pokemon = dataProvider.pokemon(at: index)
19 | coordinator.showPokemonDetailScene(pokemon: pokemon)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Features/Backpack/Sources/Scenes/Backpack Scene/BackpackDataProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackpackDataProvider.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Common
10 |
11 | public protocol BackpackDataProvider {
12 | func pokemons() -> [LocalPokemon]
13 | }
14 |
15 | extension DataProvider: BackpackDataProvider {
16 | public func pokemons() -> [LocalPokemon] {
17 | return appData.pokemons
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Features/Backpack/Sources/Scenes/Backpack Scene/BackpackDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackpackDataSource.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Haneke
11 | import Common
12 |
13 | class BackpackDataSource: NSObject, UICollectionViewDataSource {
14 |
15 | weak var presenter: BackpackPresenting?
16 |
17 | func register(collectionView: UICollectionView) {
18 | collectionView.register(cellType: PokemonCollectionViewCell.self)
19 | }
20 |
21 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
22 | guard let presenter = presenter else { return 0 }
23 |
24 | return presenter.pokemons().count
25 | }
26 |
27 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
28 | let cellType = PokemonCollectionViewCell.self
29 | let cell = collectionView.dequeue(cellType: cellType, for: indexPath)
30 |
31 | guard let presenter = presenter else { return cell }
32 |
33 | cell.name.text = presenter.pokemonName(at: indexPath.item)
34 | guard let imagePath = presenter.pokemonImagePath(at: indexPath.item) else {
35 | return cell
36 | }
37 |
38 | guard let imageURL = URL(string: imagePath) else { return cell }
39 | cell.imageView.hnk_setImage(from: imageURL, placeholder: UIImage(named: Constants.Image.pokemonPlaceholder))
40 |
41 | return cell
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Features/Backpack/Sources/Scenes/Backpack Scene/BackpackDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackpackDelegate.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class BackpackDelegate: NSObject, UICollectionViewDelegate {
12 |
13 | weak var presenter: BackpackPresenting?
14 |
15 | func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
16 | return true
17 | }
18 |
19 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
20 | // collectionView.deselectItem(at: indexPath, animated: true)
21 |
22 | guard let presenter = presenter else { return }
23 | presenter.selectItem(at: indexPath.item)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Features/Backpack/Sources/Scenes/Backpack Scene/BackpackPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackpackPresenter.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | // swiftlint:disable weak_delegate
10 |
11 | import Common
12 |
13 | protocol BackpackView: AnyObject {
14 | func setDataSource(dataSource: BackpackDataSource)
15 | }
16 |
17 | protocol BackpackPresenting: AnyObject {
18 | var dataSource: BackpackDataSource { get }
19 | var delegate: BackpackDelegate { get }
20 |
21 | func viewDidLoad()
22 | func pokemons() -> [LocalPokemon]
23 | func pokemonImagePath(at index: Int) -> String?
24 | func pokemonName(at index: Int) -> String
25 | func selectItem(at index: Int)
26 | }
27 |
28 | class BackpackPresenter: BackpackPresenting {
29 |
30 | // MARK: Properties
31 |
32 | private weak var view: BackpackView?
33 | private var actions: BackpackActions
34 | private var dataProvider: BackpackDataProvider
35 | var dataSource: BackpackDataSource
36 | var delegate: BackpackDelegate
37 |
38 | // MARK: Typealias
39 |
40 | typealias Actions = BackpackActions
41 | typealias DataProvider = BackpackDataProvider
42 | typealias View = BackpackView
43 | typealias DataSource = BackpackDataSource
44 | typealias Delegate = BackpackDelegate
45 |
46 | required init(actions: BackpackActions, dataProvider: BackpackDataProvider, view: BackpackView) {
47 | self.view = view
48 | self.actions = actions
49 | self.dataProvider = dataProvider
50 | delegate = BackpackDelegate()
51 | dataSource = BackpackDataSource()
52 | delegate.presenter = self
53 | dataSource.presenter = self
54 | }
55 |
56 | func viewDidLoad() {
57 | view?.setDataSource(dataSource: dataSource)
58 | }
59 |
60 | func pokemons() -> [LocalPokemon] {
61 | return dataProvider.pokemons()
62 | }
63 |
64 | func pokemonImagePath(at index: Int) -> String? {
65 | return pokemons()[index].spriteUrlString
66 | }
67 |
68 | func pokemonName(at index: Int) -> String {
69 | return pokemons()[index].name.capitalized
70 | }
71 |
72 | func selectItem(at index: Int) {
73 | actions.selectItem(at: index)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Features/Backpack/Sources/Scenes/Backpack Scene/BackpackViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackpackViewController.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Common
11 |
12 | public class BackpackViewController: UIViewController {
13 | var presenter: BackpackPresenting?
14 |
15 | @IBOutlet private weak var collectionView: UICollectionView!
16 |
17 | public override func viewDidLoad() {
18 | super.viewDidLoad()
19 | setupCollectionView()
20 | guard let presenter = presenter else { return }
21 | presenter.viewDidLoad()
22 |
23 | title = Constants.Translations.BackpackScene.title
24 | }
25 |
26 | private func setupCollectionView() {
27 | guard let presenter = presenter else { return }
28 | collectionView.delegate = presenter.delegate
29 | collectionView.dataSource = presenter.dataSource
30 | collectionView.reloadData()
31 | }
32 | }
33 |
34 | extension BackpackViewController: BackpackView {
35 | func setDataSource(dataSource: BackpackDataSource) {
36 | dataSource.register(collectionView: collectionView)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Features/Backpack/Sources/Scenes/Backpack Scene/BackpackWireframe.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackpackWireframe.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Common
11 |
12 | public class BackpackWireframe {
13 |
14 | public static func makeViewController() -> BackpackViewController {
15 | let storyboard = UIStoryboard.init(name: "BackpackViewController", bundle: Bundle(for: BackpackViewController.self))
16 | return BackpackViewController.instantiateFromStoryboard(storyboard: storyboard)
17 | }
18 |
19 | public static func makeNavigationController() -> UINavigationController {
20 | let viewController = makeViewController()
21 | let navigationController = UINavigationController(rootViewController: viewController)
22 | let navigationBarButton = UIBarButtonItem(title: Constants.Translations.BackpackScene.closeButton,
23 | style: .plain) { _ in
24 | viewController.dismiss(animated: true, completion: nil)
25 | }
26 |
27 | viewController.navigationItem.leftBarButtonItem = navigationBarButton
28 |
29 | return navigationController
30 | }
31 |
32 | public static func prepare(_ viewController: BackpackViewController, actions: BackpackActions, dataProvider: BackpackDataProvider) {
33 | let presenter = BackpackPresenter(actions: actions, dataProvider: dataProvider, view: viewController)
34 | viewController.presenter = presenter
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Features/Backpack/Sources/Scenes/Backpack Scene/Cells/PokemonCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PokemonCollectionViewCell.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class PokemonCollectionViewCell: UICollectionViewCell {
12 | @IBOutlet weak var imageView: UIImageView!
13 | @IBOutlet weak var name: UILabel!
14 | }
15 |
--------------------------------------------------------------------------------
/Features/Backpack/Tests/BackpackTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 |
4 | final class Backpack_UITests: XCTestCase {
5 | func test_example() {
6 | XCTAssertEqual("BackpackUI", "BackpackUI")
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Features/Catch/Example/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 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "PokeBallLogo_120.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "PokeBallLogo_180.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "1024x1024",
47 | "idiom" : "ios-marketing",
48 | "filename" : "PokeBallLogo_1024.png",
49 | "scale" : "1x"
50 | }
51 | ],
52 | "info" : {
53 | "version" : 1,
54 | "author" : "xcode"
55 | }
56 | }
--------------------------------------------------------------------------------
/Features/Catch/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Catch/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png
--------------------------------------------------------------------------------
/Features/Catch/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Catch/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png
--------------------------------------------------------------------------------
/Features/Catch/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Catch/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png
--------------------------------------------------------------------------------
/Features/Catch/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Catch/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png
--------------------------------------------------------------------------------
/Features/Catch/Example/Resources/Assets.xcassets/Ball.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Ball.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Catch/Example/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Features/Catch/Example/Resources/Assets.xcassets/PokemonPlaceholder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "PokemonPlaceholder.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Catch/Example/Resources/Assets.xcassets/PokemonPlaceholder.imageset/PokemonPlaceholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Catch/Example/Resources/Assets.xcassets/PokemonPlaceholder.imageset/PokemonPlaceholder.png
--------------------------------------------------------------------------------
/Features/Catch/Example/Sources/AppController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppController.swift
3 | // Wefox Pokedex
4 | //
5 | // Created by Ronan on 01/07/21.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | protocol AppControlling {
13 | func start()
14 | }
15 |
16 | class AppController: AppControlling {
17 | var coordinator: Coordinating?
18 |
19 | func start() {
20 | let dataProvider = DataProvider()
21 |
22 | dataProvider.start()
23 |
24 | coordinator = Coordinator()
25 | coordinator?.dataProvider = dataProvider
26 | coordinator?.start()
27 |
28 | dataProvider.notifier = coordinator as? Notifier
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Features/Catch/Example/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Common
3 |
4 | @main
5 | class AppDelegate: UIResponder, UIApplicationDelegate {
6 |
7 | private let appController = AppController()
8 |
9 | func application(
10 | _ application: UIApplication,
11 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
12 | ) -> Bool {
13 |
14 | appController.start()
15 |
16 | return true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Features/Catch/Example/Sources/Coordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Coordinator.swift
3 | // Catch
4 | //
5 | // Created by Ronan on 01/07/21.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import JGProgressHUD
11 | import Common
12 | import NetworkKit
13 | import CatchUI
14 |
15 | class Coordinator: Coordinating {
16 | let window: UIWindow
17 | var dataProvider: DataProvider?
18 | var hud: JGProgressHUD?
19 | lazy var actions = Actions(coordinator: self)
20 | var presenter: Updatable?
21 | var currentViewController: UIViewController?
22 |
23 | init() {
24 | window = UIWindow(frame: UIScreen.main.bounds)
25 | window.makeKeyAndVisible()
26 | }
27 |
28 | func start() {
29 | actions.dataProvider = dataProvider
30 |
31 | showHomeScene()
32 | }
33 |
34 | func showHomeScene() {
35 | guard let dataProvider = dataProvider else { return }
36 | let viewController = HomeWireframe.makeViewController()
37 | HomeWireframe.prepare(viewController, actions: actions as HomeActions, dataProvider: dataProvider as HomeDataProvider)
38 |
39 | window.rootViewController = viewController
40 | }
41 |
42 | func showCatchScene() {
43 | guard let dataProvider = dataProvider else { return }
44 | let viewController = CatchWireframe.makeViewController()
45 |
46 | CatchWireframe.prepare(viewController, actions: actions as CatchActions, dataProvider: dataProvider as CatchDataProvider)
47 |
48 | guard let topViewController = window.rootViewController else { return }
49 |
50 | topViewController.present(viewController, animated: true, completion: nil)
51 |
52 | presenter = viewController.presenter as? Updatable
53 |
54 | currentViewController = viewController
55 |
56 | searchNextPokemon()
57 |
58 | showLoading()
59 | }
60 |
61 | func searchNextPokemon() {
62 | guard let dataProvider = dataProvider else { return }
63 | dataProvider.search(identifier: Generator.nextIdentifier(), networkService: PokemonSearchService())
64 | }
65 |
66 | func showBackpackScene() {
67 |
68 | }
69 |
70 | func showPokemonDetailScene(pokemon: LocalPokemon) {
71 |
72 | }
73 |
74 | func showLoading() {
75 | showHud(with: Constants.Translations.loading)
76 | }
77 |
78 | private func showHud(with message: String) {
79 | guard let viewController = currentViewController else { return }
80 | hud = JGProgressHUD(style: .dark)
81 | hud?.textLabel.text = message
82 | hud?.show(in: viewController.view)
83 | }
84 |
85 | func dismissLoading() {
86 | hud?.dismiss(animated: true)
87 | hud = nil
88 | }
89 |
90 | func showAlert(with message: String) {
91 | let alertController = UIAlertController(title: nil,
92 | message: message,
93 | preferredStyle: .alert)
94 |
95 | let okButton = UIAlertAction(title: Constants.Translations.ok,
96 | style: .default,
97 | handler: nil)
98 |
99 | alertController.addAction(okButton)
100 |
101 | guard let viewController = currentViewController else { return }
102 |
103 | viewController.present(alertController,
104 | animated: true,
105 | completion: nil)
106 | }
107 | }
108 |
109 | extension Coordinator: Notifier {
110 | func dataReceived(errorMessage: String?, on queue: DispatchQueue?) {
111 |
112 | var localQueue = queue
113 |
114 | if localQueue == nil {
115 | localQueue = .global(qos: .userInteractive)
116 | }
117 |
118 | localQueue?.async {
119 | self.dismissLoading()
120 |
121 | if let errorMessage = errorMessage {
122 | if errorMessage == Constants.Translations.Error.statusCode404 {
123 | self.presenter?.update()
124 | return
125 | }
126 | self.presenter?.showError(message: errorMessage)
127 | } else {
128 | self.presenter?.update()
129 | }
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/Features/Catch/Example/Sources/DataProviderExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataProviderExtension.swift
3 | // PokedexCommon
4 | //
5 | // Created by Ronan O Ciosig on 01/07/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import NetworkKit
11 | import os.log
12 | import Common
13 |
14 | public protocol DataSearchProviding {
15 | func search(identifier: Int, networkService: SearchService)
16 | }
17 |
18 | extension DataProvider: DataSearchProviding {
19 | public func search(identifier: Int, networkService: SearchService) {
20 | appData.pokemon = nil
21 | let queue = DispatchQueue.main
22 |
23 | searchCancellable = networkService.search(identifier: identifier)
24 | .receive(on: queue)
25 | .sink(receiveCompletion: { completion in
26 | switch completion {
27 | case .failure(let error):
28 | let errorMessage = "\(error.localizedDescription)"
29 | os_log("Error: %s", log: Log.data, type: .error, errorMessage)
30 | self.notifier?.dataReceived(errorMessage: errorMessage, on: queue)
31 | case .finished:
32 | print("All good")
33 | }
34 |
35 | }, receiveValue: { data in
36 | do {
37 | let decoder = JSONDecoder()
38 | decoder.keyDecodingStrategy = .convertFromSnakeCase
39 | let pokemon = try decoder.decode(Pokemon.self, from: data)
40 | self.appData.pokemon = pokemon
41 | self.notifier?.dataReceived(errorMessage: nil, on: queue)
42 | os_log("Success: %s", log: Log.network, type: .default, "Loaded")
43 | } catch {
44 | let errorMessage = "\(error.localizedDescription)"
45 | os_log("Error: %s", log: Log.data, type: .error, errorMessage)
46 | self.notifier?.dataReceived(errorMessage: errorMessage, on: queue)
47 | }
48 | })
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Features/Catch/Example/Sources/Log.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Log.swift
3 | // PokedexCommon
4 | //
5 | // Created by Ronan O Ciosig on01/07/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 |
12 | public struct Log {
13 | public static var general = OSLog(subsystem: "com.sonomos.pokedex", category: "general")
14 | public static var network = OSLog(subsystem: "com.sonomos.pokedex", category: "network")
15 | public static var data = OSLog(subsystem: "com.sonomos.pokedex", category: "data")
16 | }
17 |
--------------------------------------------------------------------------------
/Features/Catch/Example/Sources/Scenes/Home Scene/HomeActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewActions.swift
3 | // Catch
4 | //
5 | // Created by Ronan O Ciosig on 01/07/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | protocol HomeActions {
13 | func catchButtonAction()
14 | }
15 |
16 | extension Actions: HomeActions {
17 | func catchButtonAction() {
18 | coordinator.showCatchScene()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Features/Catch/Example/Sources/Scenes/Home Scene/HomeDataProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeDataProvider.swift
3 | // Catch
4 | //
5 | // Created by Ronan O Ciosig on 01/07/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Common
10 |
11 | protocol HomeDataProvider {
12 |
13 | }
14 |
15 | extension DataProvider: HomeDataProvider {
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Features/Catch/Example/Sources/Scenes/Home Scene/HomePresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomePresenter.swift
3 | // Catch
4 | //
5 | // Created by Ronan O Ciosig on 01/07/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol HomeView: AnyObject {
12 |
13 | }
14 |
15 | protocol HomePresenting: AnyObject {
16 | func catchButtonAction()
17 | }
18 |
19 | class HomePresenter: HomePresenting {
20 |
21 | // MARK: Properties
22 |
23 | private weak var view: HomeView?
24 | private var actions: HomeActions
25 | private var dataProvider: HomeDataProvider
26 |
27 | // MARK: Typealias
28 |
29 | typealias Actions = HomeActions
30 | typealias DataProvider = HomeDataProvider
31 | typealias View = HomeView
32 |
33 | required init(view: HomeView, actions: HomeActions, dataProvider: HomeDataProvider) {
34 | self.view = view
35 | self.actions = actions
36 | self.dataProvider = dataProvider
37 | }
38 |
39 | func catchButtonAction() {
40 | actions.catchButtonAction()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Features/Catch/Example/Sources/Scenes/Home Scene/HomeViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Features/Catch/Example/Sources/Scenes/Home Scene/HomeViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewController.swift
3 | // Catch
4 | //
5 | // Created by Ronan O Ciosig on 01/07/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class HomeViewController: UIViewController {
12 | var presenter: HomePresenting?
13 |
14 | @IBAction func catchButtonAction() {
15 | guard let presenter = presenter else { return }
16 | presenter.catchButtonAction()
17 | }
18 | }
19 |
20 | extension HomeViewController: HomeView {
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/Features/Catch/Example/Sources/Scenes/Home Scene/HomeWireframe.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeWireframe.swift
3 | // Catch
4 | //
5 | // Created by Ronan O Ciosig on 01/07/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class HomeWireframe {
12 |
13 | static func makeViewController() -> HomeViewController {
14 | let storyboard = UIStoryboard.init(name: "HomeViewController", bundle: nil)
15 | return HomeViewController.instantiateFromStoryboard(storyboard: storyboard)
16 | }
17 |
18 | static func prepare(_ viewController: HomeViewController, actions: HomeActions, dataProvider: HomeDataProvider) {
19 | let presenter = HomePresenter(view: viewController, actions: actions, dataProvider: dataProvider)
20 | viewController.presenter = presenter
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/Features/Catch/Example/Tests/AppTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 |
4 | final class CatchTests: XCTestCase {
5 | func test_twoPlusTwo_isFour() {
6 | XCTAssertEqual(2+2, 4)
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Features/Catch/Resources/Assets.xcassets/Background.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "pokemonBackground.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Features/Catch/Resources/Assets.xcassets/Background.imageset/pokemonBackground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Catch/Resources/Assets.xcassets/Background.imageset/pokemonBackground.png
--------------------------------------------------------------------------------
/Features/Catch/Resources/Assets.xcassets/Ball.imageset/Ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Catch/Resources/Assets.xcassets/Ball.imageset/Ball.png
--------------------------------------------------------------------------------
/Features/Catch/Resources/Assets.xcassets/Ball.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Ball.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Catch/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Features/Catch/Resources/Assets.xcassets/PokemonPlaceholder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "PokemonPlaceholder.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Catch/Resources/Assets.xcassets/PokemonPlaceholder.imageset/PokemonPlaceholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Catch/Resources/Assets.xcassets/PokemonPlaceholder.imageset/PokemonPlaceholder.png
--------------------------------------------------------------------------------
/Features/Catch/Sources/Scenes/Catch Scene/CatchActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CatchActions.swift
3 | // CatchUI
4 | //
5 | // Created by Ronan on 01/07/21.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import Common
10 |
11 | public protocol CatchActions {
12 | func catchPokemon()
13 | }
14 |
15 | extension Actions: CatchActions {
16 | public func catchPokemon() {
17 | dataProvider?.catchPokemon()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Features/Catch/Sources/Scenes/Catch Scene/CatchDataProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CatchDataProvider.swift
3 | // CatchUI
4 | //
5 | // Created by Ronan on 01/07/21.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | public protocol CatchDataProvider {
13 | func pokemon() -> ScreenPokemon?
14 | func newSpecies() -> Bool
15 | }
16 |
17 | extension DataProvider: CatchDataProvider {
18 | public func pokemon() -> ScreenPokemon? {
19 | guard let foundPokemon = appData.pokemon else { return nil }
20 | return ScreenPokemon(name: foundPokemon.name,
21 | weight: foundPokemon.weight,
22 | height: foundPokemon.height,
23 | iconPath: foundPokemon.sprites.frontDefault)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Features/Catch/Sources/Scenes/Catch Scene/CatchPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CatchPresenter.swift
3 | // CatchUI
4 | //
5 | // Created by Ronan on 01/07/21.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import Common
10 |
11 | protocol CatchView: AnyObject {
12 | func update()
13 | func showLeaveOrCatchAlert()
14 | func showLeaveItAlert()
15 | func showNotFoundAlert()
16 | func showError(message: String)
17 | }
18 |
19 | public protocol CatchPresenting: AnyObject {
20 | func pokemon() -> ScreenPokemon?
21 | func catchPokemonAction()
22 | }
23 |
24 | public class CatchPresenter: CatchPresenting, Updatable {
25 |
26 | // MARK: Properties
27 |
28 | private weak var view: CatchView?
29 | private var actions: CatchActions
30 | private var dataProvider: CatchDataProvider
31 |
32 | // MARK: Typealias
33 |
34 | typealias Actions = CatchActions
35 | typealias DataProvider = CatchDataProvider
36 | typealias View = CatchView
37 |
38 | required init(view: CatchView, actions: CatchActions, dataProvider: CatchDataProvider) {
39 | self.view = view
40 | self.actions = actions
41 | self.dataProvider = dataProvider
42 | }
43 |
44 | public func update() {
45 | guard let view = view else { return }
46 | view.update()
47 |
48 | if pokemon() == nil {
49 | view.showNotFoundAlert()
50 | return
51 | }
52 |
53 | dataProvider.newSpecies() ? view.showLeaveOrCatchAlert() : view.showLeaveItAlert()
54 | }
55 |
56 | public func showError(message: String) {
57 | view?.showError(message: message)
58 | }
59 |
60 | public func pokemon() -> ScreenPokemon? {
61 | return dataProvider.pokemon()
62 | }
63 |
64 | public func catchPokemonAction() {
65 | actions.catchPokemon()
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Features/Catch/Sources/Scenes/Catch Scene/CatchWireframe.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CatchWireframe.swift
3 | // CatchUI
4 | //
5 | // Created by Ronan on 01/07/21.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public class CatchWireframe {
12 |
13 | public static func makeViewController() -> CatchViewController {
14 | let storyboard = UIStoryboard.init(name: "CatchViewController", bundle: Bundle(for: CatchViewController.self))
15 | return CatchViewController.instantiateFromStoryboard(storyboard: storyboard)
16 | }
17 |
18 | public static func prepare(_ viewController: CatchViewController,
19 | actions: CatchActions,
20 | dataProvider: CatchDataProvider) {
21 | let presenter = CatchPresenter(view: viewController,
22 | actions: actions,
23 | dataProvider: dataProvider)
24 | viewController.presenter = presenter
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Features/Catch/Tests/CatchUITests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 |
4 | final class CatchUITests: XCTestCase {
5 | func test_example() {
6 | XCTAssertEqual("CatchUI", "CatchUI")
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Core/Actions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Actions.swift
3 | // Common
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class Actions {
12 | public let coordinator: Coordinating
13 | public var dataProvider: DataProviding?
14 |
15 | public init(coordinator: Coordinating) {
16 | self.coordinator = coordinator
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Core/AppData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppData.swift
3 | // Common
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class AppData {
12 | public static let pokemonFile = "pokemons.json"
13 |
14 | public var pokemon: Pokemon?
15 | public var pokemons = [LocalPokemon]()
16 |
17 | let storage: Storable
18 |
19 | public init(storage: Storable) {
20 | self.storage = storage
21 | }
22 |
23 | public func newSpecies() -> Bool {
24 | guard let pokemon = pokemon else { return false }
25 |
26 | if pokemons.isEmpty {
27 | return true
28 | }
29 |
30 | let foundSpecies = pokemons.filter {
31 | $0.species == pokemon.species.name
32 | }
33 |
34 | return foundSpecies.isEmpty
35 | }
36 |
37 | func load() {
38 | pokemons = storage.load(AppData.pokemonFile, from: directory(), as: [LocalPokemon].self) ?? [LocalPokemon]()
39 | }
40 |
41 | func save() {
42 | storage.save(pokemons, to: directory(), as: AppData.pokemonFile)
43 | }
44 |
45 | public func directory() -> Directory {
46 | if Configuration.uiTesting == true {
47 | return .caches
48 | }
49 | return .documents
50 | }
51 |
52 | public func sortByOrder() {
53 | pokemons.sort(by: {
54 | $0.order < $1.order
55 | })
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Core/Configuration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Configuration.swift
3 | // Common
4 | //
5 | // Created by Ronan on 09/02/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public struct Configuration {
12 |
13 | public static var uiTesting: Bool {
14 | let arguments = ProcessInfo.processInfo.arguments
15 | return arguments.contains("UITesting")
16 | }
17 |
18 | public static var networkTesting: Bool {
19 | let arguments = CommandLine.arguments
20 | return arguments.contains("NetworkTesting")
21 | }
22 |
23 | public static var searchErrorTesting: Bool {
24 | return CommandLine.arguments.contains("Error_401")
25 | }
26 |
27 | public static var asyncTesting: Bool {
28 | let arguments = ProcessInfo.processInfo.arguments
29 | return arguments.contains("AsyncTesting")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Core/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // Common
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // swiftlint:disable nesting identifier_name
12 |
13 | public struct Constants {
14 |
15 | public struct Network {
16 | public static let baseUrlPath = "https://pokeapi.co/api/v2/"
17 | public static let searchPath = "pokemon"
18 | }
19 |
20 | public struct Image {
21 | public static let pokemonPlaceholder = "PokemonPlaceholder"
22 | }
23 |
24 | public struct Translations {
25 | public static let loading = "Loading"
26 | public static let ok = "OK"
27 | public static let cancel = "Cancel"
28 |
29 | public struct HomeScene {
30 | public static let catchTitle = "Catch a Pokemon"
31 | }
32 |
33 | public struct CatchScene {
34 | public static let weight = "WEIGHT"
35 | public static let height = "HEIGHT"
36 | public static let leaveOrCatchAlertMessageTitle = "Do you want to leave it or catch it?"
37 | public static let leaveItButtonTitle = "Leave it"
38 | public static let catchItButtonTitle = "Catch it!"
39 | public static let alreadyHaveItAlertMessageTitle = "You have already caught one of this species, you'll have to leave this one..."
40 |
41 | public static let noPokemonFoundAlertTitle = "No Pokemon found, you will have to try again."
42 | }
43 |
44 | public struct BackpackScene {
45 | public static let title = "Backpack"
46 | public static let closeButton = "Close"
47 | }
48 |
49 | public struct DetailScene {
50 | public static let weight = "Weight"
51 | public static let height = "Height"
52 | public static let date = "Date"
53 | public static let experience = "Experience"
54 | public static let types = "Types"
55 | }
56 |
57 | public struct Error {
58 | public static let jsonDecodingError = "Error: JSON decoding error."
59 | public static let noDataError = "Error: No data received."
60 | public static let noResultsFound = "No results were found for your search."
61 | public static let statusCode404 = "404"
62 | public static let notFound = "Error 401 Pokemon not found"
63 | public static let asyncError = "Async Search failed"
64 | }
65 | }
66 |
67 | public struct PokemonAPI {
68 | public static let minIdentifier = 1
69 | public static let maxIdentifier = 1000
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Core/Coordinating.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Coordinating.swift
3 | // Common
4 | //
5 | // Created by Ronan O Ciosig on 19/5/21.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Coordinating {
12 | var dataProvider: DataProvider? { get set }
13 |
14 | func start()
15 | func showLoading()
16 | func dismissLoading()
17 | func showHomeScene()
18 | func showCatchScene()
19 | func showBackpackScene()
20 | func showPokemonDetailScene(pokemon: LocalPokemon)
21 | func showAlert(with errorMessage: String)
22 | }
23 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Core/DataProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataProvider.swift
3 | // Common
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 | import Combine
12 |
13 | public class DataProvider: DataProviding {
14 | public let appData = AppData(storage: FileStorage())
15 | public var notifier: Notifier?
16 | public var searchCancellable: AnyCancellable?
17 |
18 | public init() {
19 |
20 | }
21 |
22 | public func start() {
23 | appData.load()
24 | appData.sortByOrder()
25 | }
26 |
27 | public func catchPokemon() {
28 | guard let pokemon = appData.pokemon else { return }
29 | let localPokemon = PokemonParser.parse(pokemon: pokemon)
30 | appData.pokemons.append(localPokemon)
31 | appData.sortByOrder()
32 | appData.save()
33 | }
34 |
35 | public func newSpecies() -> Bool {
36 | return appData.newSpecies()
37 | }
38 |
39 | public func pokemon(at index: Int) -> LocalPokemon {
40 | return appData.pokemons[index]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Core/DataProviding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataProviding.swift
3 | // Common
4 | //
5 | // Created by Ronan O Ciosig on 5/6/21.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol DataProviding {
12 | func catchPokemon()
13 | func newSpecies() -> Bool
14 | func pokemon(at index: Int) -> LocalPokemon
15 | }
16 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Core/Notifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Notifier.swift
3 | // Common
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Notifier {
12 | func dataReceived(errorMessage: String?, on queue: DispatchQueue?)
13 | }
14 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Core/Updatable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Updatable.swift
3 | // Common
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Updatable {
12 | func update()
13 | func showError(message: String)
14 | }
15 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Extensions/Loadable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Loadable.swift
3 | // Common
4 | //
5 | // Created by Ronan on 26/11/2018.
6 | // Copyright © 2018 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Lodable protocol to load from nib
12 | public protocol Loadable { }
13 |
14 | extension UIView: Loadable { }
15 |
16 | /// Loadbale to laod nib from file
17 | public extension Loadable where Self: UIView {
18 |
19 | /**
20 | load `UIView` from xib
21 |
22 | - Parameters:
23 | - frame: CGRect default = nil
24 | - bundle: default = Bundle.main
25 | */
26 | static func loadFromNib(withFrame frame: CGRect? = nil, bundle: Bundle = Bundle(for: Self.self)) -> Self? {
27 | guard let view = bundle.loadNibNamed(staticIdentifier, owner: nil, options: nil)?.last as? Self else { return nil }
28 | view.frame = frame ?? view.frame
29 | return view
30 | }
31 |
32 | private static var staticIdentifier: String {
33 | return String(describing: self)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Extensions/UIBarButtonItem+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIBarButtonItem+Extension.swift
3 | // Common
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public extension UIBarButtonItem {
12 |
13 | /// Typealias for UIBarButtonItem closure.
14 | typealias UIBarButtonItemTargetClosure = (UIBarButtonItem) -> Void
15 |
16 | private class UIBarButtonItemClosureWrapper: NSObject {
17 | let closure: UIBarButtonItemTargetClosure
18 | init(_ closure: @escaping UIBarButtonItemTargetClosure) {
19 | self.closure = closure
20 | }
21 | }
22 |
23 | private struct AssociatedKeys {
24 | static var targetClosure = "targetClosure"
25 | }
26 |
27 | var targetClosure: UIBarButtonItemTargetClosure? {
28 | get {
29 | guard let closureWrapper = objc_getAssociatedObject(self, &AssociatedKeys.targetClosure) as? UIBarButtonItemClosureWrapper else { return nil }
30 | return closureWrapper.closure
31 | }
32 | set(newValue) {
33 | guard let newValue = newValue else { return }
34 | objc_setAssociatedObject(self, &AssociatedKeys.targetClosure, UIBarButtonItemClosureWrapper(newValue), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
35 | }
36 | }
37 |
38 | convenience init(title: String?, style: UIBarButtonItem.Style, closure: @escaping UIBarButtonItemTargetClosure) {
39 | self.init(title: title, style: style, target: nil, action: nil)
40 | targetClosure = closure
41 | action = #selector(UIBarButtonItem.closureAction)
42 | }
43 |
44 | @objc func closureAction() {
45 | guard let targetClosure = targetClosure else { return }
46 | targetClosure(self)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Extensions/UITableViewCell+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UITableViewCell+Extension.swift
3 | // Common
4 | //
5 | // Created by Ronan on 26/11/2018.
6 | // Copyright © 2018 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UITableViewCell {
12 |
13 | /// UITableViewCell identifier
14 | public static var identifier: String {
15 | return String(describing: self)
16 | }
17 | }
18 |
19 | extension UITableView {
20 |
21 | /// Register UITableViewCell type
22 | public func register(cellType: UITableViewCell.Type) {
23 | self.register(UINib(nibName: cellType.identifier, bundle: Bundle(for: cellType.self)), forCellReuseIdentifier: cellType.identifier)
24 | }
25 |
26 | /// Register UITableViewCell types
27 | // public func register(cellTypes: UITableViewCell.Type...) {
28 | // cellTypes.forEach(register)
29 | // }
30 |
31 | /**
32 | Dequeue generic type `element` of `UITableViewCell` for `indexPath`
33 |
34 | - Parameters:
35 | - cellType: Element.Type
36 | - indexPath: header for `IndexPath`
37 | */
38 | public func dequeue(cellType: Element.Type, for indexPath: IndexPath) -> Element {
39 | let cell = dequeueReusableCell(withIdentifier: cellType.identifier, for: indexPath)
40 |
41 | guard let element = cell as? Element else {
42 | fatalError("Cell \(cell) cannot be casted as \(cellType.identifier)")
43 | }
44 |
45 | return element
46 | }
47 | }
48 |
49 | extension UICollectionViewCell {
50 |
51 | /// UITableViewCell identifier
52 | public static var identifier: String {
53 | return String(describing: self)
54 | }
55 | }
56 |
57 | extension UICollectionView {
58 |
59 | /// Register UITableViewCell type
60 | public func register(cellType: UICollectionViewCell.Type) {
61 | self.register(UINib(nibName: cellType.identifier, bundle: Bundle(for: cellType.self)), forCellWithReuseIdentifier: cellType.identifier)
62 | }
63 |
64 | /**
65 | Dequeue generic type `element` of `UICollectionViewCell` for `indexPath`
66 |
67 | - Parameters:
68 | - cellType: Element.Type
69 | - indexPath: header for `IndexPath`
70 | */
71 | public func dequeue(cellType: Element.Type, for indexPath: IndexPath) -> Element {
72 | let cell = dequeueReusableCell(withReuseIdentifier: cellType.identifier, for: indexPath)
73 |
74 | guard let element = cell as? Element else {
75 | fatalError("Cell \(cell) cannot be casted as \(cellType.identifier)")
76 | }
77 |
78 | return element
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Extensions/UIViewController+StoryboardInstantiable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+StoryboardInstantiable.swift
3 | // InstantiateFromStoryboard
4 | //
5 | // Created by Eugenio Baglieri on 30/12/15.
6 | // Copyright © 2015 Eugenio Baglieri. All rights reserved.
7 | //
8 |
9 | // swiftlint:disable all
10 |
11 | import UIKit
12 |
13 | public protocol StoryboardInstantiable {
14 | static var storyboardIdentifier: String { get }
15 | static func instantiateFromStoryboard(storyboard: UIStoryboard) -> Self
16 | }
17 |
18 | extension UIViewController: StoryboardInstantiable {
19 | public static var storyboardIdentifier: String {
20 | // Get the name of current class
21 | let classString = NSStringFromClass(self)
22 | let components = classString.components(separatedBy: ".")
23 | assert(components.count > 0, "Failed extract class name from \(classString)")
24 | return components.last!
25 | }
26 |
27 | public class func instantiateFromStoryboard(storyboard: UIStoryboard) -> Self {
28 | return instantiateFromStoryboard(storyboard: storyboard, type: self)
29 | }
30 | }
31 |
32 | extension UIViewController {
33 |
34 | // Thanks to generics, return automatically the right type
35 | private class func instantiateFromStoryboard(storyboard: UIStoryboard, type: T.Type) -> T {
36 | return storyboard.instantiateViewController(withIdentifier: self.storyboardIdentifier) as! T
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Model/Generator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Generator.swift
3 | // Common
4 | //
5 | // Created by Ronan on 10/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Generator {
12 | public static func nextIdentifier() -> Int {
13 | return Int.random(in: Constants.PokemonAPI.minIdentifier.. LocalPokemon {
13 | let types = pokemon.types
14 |
15 | let typeNames = types.map {
16 | $0.type.name
17 | }
18 |
19 | return LocalPokemon(name: pokemon.name,
20 | weight: pokemon.weight,
21 | height: pokemon.height,
22 | order: pokemon.order,
23 | spriteUrlString: pokemon.sprites.frontDefault,
24 | date: Date(),
25 | species: pokemon.species.name, baseExperience: pokemon.baseExperience, types: typeNames)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Model/ScreenPokemon.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenPokemon.swift
3 | // Common
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public struct ScreenPokemon {
12 | public init(name: String, weight: Int, height: Int, iconPath: String?) {
13 | self.name = name
14 | self.weight = weight
15 | self.height = height
16 | self.iconPath = iconPath
17 | }
18 |
19 | public let name: String
20 | public let weight: Int
21 | public let height: Int
22 | public var iconPath: String?
23 | }
24 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Util/FileStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileStorage.swift
3 | // Common
4 | //
5 | // Created by Ronan on 23/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum Directory {
12 | case documents
13 | case caches
14 | }
15 |
16 | public protocol Storable {
17 | func fileExists(fileName: String, in directory: Directory) -> Bool
18 | func save(_ object: T, to directory: Directory, as fileName: String)
19 | func load(_ fileName: String, from directory: Directory, as type: T.Type) -> T?
20 | func remove(_ fileName: String, from directory: Directory)
21 | }
22 |
23 | public class FileStorage: Storable {
24 | public init() {
25 |
26 | }
27 | public func save(_ object: T, to directory: Directory, as fileName: String) where T: Encodable {
28 | Storage.store(object, to: directoryAdaptor(directory: directory), as: fileName)
29 | }
30 |
31 | public func load(_ fileName: String, from directory: Directory, as type: T.Type) -> T? where T: Decodable {
32 | if fileExists(fileName: fileName, in: directory) {
33 | return Storage.retrieve(fileName, from: directoryAdaptor(directory: directory), as: T.self)
34 | }
35 |
36 | return nil
37 | }
38 |
39 | public func fileExists(fileName: String, in directory: Directory) -> Bool {
40 | return Storage.fileExists(AppData.pokemonFile, in: directoryAdaptor(directory: directory))
41 | }
42 |
43 | public func remove(_ fileName: String, from directory: Directory) {
44 | Storage.remove(fileName, from: directoryAdaptor(directory: directory))
45 | }
46 |
47 | private func directoryAdaptor(directory: Directory) -> Storage.Directory {
48 | switch directory {
49 | case Directory.documents:
50 | return Storage.Directory.documents
51 | case Directory.caches:
52 | return Storage.Directory.caches
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Features/Common/Sources/Views/PokemonView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PokemonView.swift
3 | // Common
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @IBDesignable
12 | public class PokemonView: UIView {
13 | @IBOutlet public weak var name: UILabel!
14 | @IBOutlet public weak var weight: UILabel!
15 | @IBOutlet public weak var height: UILabel!
16 | @IBOutlet public weak var imageView: UIImageView!
17 | @IBOutlet public weak var date: UILabel!
18 | @IBOutlet public weak var types: UILabel!
19 | @IBOutlet public weak var experience: UILabel!
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Features/Common/Tests/CommonTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommonUnitTests.swift
3 | // CommonTests
4 | //
5 | // Created by Ronan.
6 | // Copyright © 2021. All rights reserved.
7 | //
8 |
9 | @testable import Common
10 |
11 | import XCTest
12 |
13 | class CommonTests: XCTestCase {
14 | func testNextIdentifierGenerator() {
15 | // Given
16 |
17 | // When
18 | let identifier = Generator.nextIdentifier()
19 | // Then
20 | XCTAssertTrue(identifier < Constants.PokemonAPI.maxIdentifier)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Features/Detail/Example/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 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "PokeBallLogo_120.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "PokeBallLogo_180.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "1024x1024",
47 | "idiom" : "ios-marketing",
48 | "filename" : "PokeBallLogo_1024.png",
49 | "scale" : "1x"
50 | }
51 | ],
52 | "info" : {
53 | "version" : 1,
54 | "author" : "xcode"
55 | }
56 | }
--------------------------------------------------------------------------------
/Features/Detail/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Detail/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png
--------------------------------------------------------------------------------
/Features/Detail/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Detail/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png
--------------------------------------------------------------------------------
/Features/Detail/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Detail/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png
--------------------------------------------------------------------------------
/Features/Detail/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Detail/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png
--------------------------------------------------------------------------------
/Features/Detail/Example/Resources/Assets.xcassets/Ball.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Ball.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Detail/Example/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Features/Detail/Example/Resources/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 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/Features/Detail/Example/Sources/AppController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppController.swift
3 | // DetailExample
4 | //
5 | // Created by Ronan on 12/09/2021.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | protocol AppControlling {
13 | func start()
14 | }
15 |
16 | class AppController: AppControlling {
17 | var coordinator: Coordinating?
18 |
19 | func start() {
20 | let dataProvider = DataProvider()
21 |
22 | if Configuration.uiTesting == true {
23 | let storage = FileStorage()
24 | storage.remove(AppData.pokemonFile, from: dataProvider.appData.directory())
25 | }
26 |
27 | dataProvider.start()
28 | dataProvider.notifier = coordinator as? Notifier
29 | dataProvider.appData.pokemons = MockDataFactory.makePokemons()
30 |
31 | coordinator = Coordinator()
32 | coordinator?.dataProvider = dataProvider
33 | coordinator?.start()
34 |
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Features/Detail/Example/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // DetailExample
4 | //
5 | // Created by Ronan on 12/09/2021.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: NSObject, UIApplicationDelegate {
13 |
14 | private let appController = AppController()
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | appController.start()
18 | return true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Features/Detail/Example/Sources/Coordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Coordinator.swift
3 | // DetailExample
4 | //
5 | // Created by Ronan on 12/09/2021.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Common
11 | import Detail
12 |
13 | class Coordinator: Coordinating {
14 | let window: UIWindow
15 | var dataProvider: DataProvider?
16 |
17 | lazy var actions = Actions(coordinator: self)
18 | var presenter: Updatable?
19 | var currentViewController: UIViewController?
20 |
21 | init() {
22 | window = UIWindow(frame: UIScreen.main.bounds)
23 | window.makeKeyAndVisible()
24 | }
25 |
26 | func start() {
27 | actions.dataProvider = dataProvider
28 |
29 | showHomeScene()
30 | }
31 |
32 | func showHomeScene() {
33 | guard let dataProvider = dataProvider else { return }
34 | let viewController = PokemonDetailWireframe.makeViewController()
35 | guard let pokemon = dataProvider.appData.pokemons.first else { return }
36 |
37 | PokemonDetailWireframe.prepare(viewController, pokemon: pokemon)
38 |
39 | window.rootViewController = viewController
40 | }
41 |
42 | func showCatchScene() {
43 |
44 | }
45 |
46 | func searchNextPokemon() {
47 |
48 | }
49 |
50 | func showBackpackScene() {
51 |
52 | }
53 |
54 | func showPokemonDetailScene(pokemon: LocalPokemon) {
55 |
56 | }
57 |
58 | func showLoading() {
59 | showHud(with: Constants.Translations.loading)
60 | }
61 |
62 | private func showHud(with message: String) {
63 |
64 | }
65 |
66 | func dismissLoading() {
67 |
68 | }
69 |
70 | func showAlert(with message: String) {
71 | let alertController = UIAlertController(title: nil,
72 | message: message,
73 | preferredStyle: .alert)
74 |
75 | let okButton = UIAlertAction(title: Constants.Translations.ok,
76 | style: .default,
77 | handler: nil)
78 |
79 | alertController.addAction(okButton)
80 |
81 | guard let viewController = currentViewController else { return }
82 |
83 | viewController.present(alertController,
84 | animated: true,
85 | completion: nil)
86 | }
87 | }
88 |
89 | extension Coordinator: Notifier {
90 | func dataReceived(errorMessage: String?, on queue: DispatchQueue?) {
91 |
92 | var localQueue = queue
93 |
94 | if localQueue == nil {
95 | localQueue = .global(qos: .userInteractive)
96 | }
97 |
98 | localQueue?.async {
99 | self.dismissLoading()
100 |
101 | if let errorMessage = errorMessage {
102 | if errorMessage == Constants.Translations.Error.statusCode404 {
103 | self.presenter?.update()
104 | return
105 | }
106 | self.presenter?.showError(message: errorMessage)
107 | } else {
108 | self.presenter?.update()
109 | }
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/Features/Detail/Example/Sources/MockDataFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockDataFactory.swift
3 | // Backpack
4 | //
5 | // Created by Ronan O Ciosig on 19/6/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | struct MockDataFactory {
13 | static func makePokemons() -> [LocalPokemon] {
14 |
15 | let pokemon1 = LocalPokemon(name: "cascoon",
16 | weight: 115,
17 | height: 7,
18 | order: 350,
19 | spriteUrlString: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/268.png",
20 | date: Date(),
21 | species: "cascoon",
22 | baseExperience: 72,
23 | types: ["bug"])
24 | let pokemon2 = LocalPokemon(name: "cranidos",
25 | weight: 315,
26 | height: 9,
27 | order: 519,
28 | spriteUrlString: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/408.png",
29 | date: Date(),
30 | species: "cranidos",
31 | baseExperience: 70,
32 | types: ["rock"])
33 |
34 | return [pokemon1, pokemon2]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Features/Detail/Sources/Scenes/DetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailViewController.swift
3 | // Detail
4 | //
5 | // Created by Ronan on 12/09/2021.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public class DetailViewController: UIViewController {
12 | let label = UILabel()
13 | public override init(nibName nib: String?, bundle: Bundle?) {
14 | super.init(nibName: nib, bundle: bundle)
15 | }
16 | required init?(coder: NSCoder) {
17 | fatalError("init(coder:) has not been implemented")
18 | }
19 | public override func viewDidLoad() {
20 | title = "Detail"
21 | view.backgroundColor = .white
22 |
23 | label.text = title
24 | label.textAlignment = .center
25 | label.translatesAutoresizingMaskIntoConstraints = false
26 | view.addSubview(label)
27 | NSLayoutConstraint.activate([
28 | label.leadingAnchor.constraint(equalTo: view.leadingAnchor),
29 | label.trailingAnchor.constraint(equalTo: view.trailingAnchor),
30 | label.heightAnchor.constraint(equalToConstant: 40),
31 | label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
32 | label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
33 | ])
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Features/Detail/Sources/Scenes/Pokemon Detail Scene/PokemonDetailPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PokemonDetailPresenter.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 10/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | protocol PokemonDetailView: AnyObject {
13 |
14 | }
15 |
16 | protocol PokemonDetailPresenting: AnyObject {
17 | func weight() -> String
18 | func name() -> String
19 | func height() -> String
20 | func imagePath() -> String?
21 | func baseExperience() -> String
22 | func date() -> String
23 | func types() -> String
24 | }
25 |
26 | class PokemonDetailPresenter: PokemonDetailPresenting {
27 |
28 | // MARK: Properties
29 | private let pokemon: LocalPokemon
30 | private weak var view: PokemonDetailView?
31 |
32 | // MARK: Typealias
33 | typealias View = PokemonDetailView
34 |
35 | required init(view: PokemonDetailView, pokemon: LocalPokemon) {
36 | self.view = view
37 | self.pokemon = pokemon
38 | }
39 |
40 | func weight() -> String {
41 | return "\(Constants.Translations.DetailScene.weight): \(pokemon.weight)"
42 | }
43 |
44 | func height() -> String {
45 | return "\(Constants.Translations.DetailScene.height): \(pokemon.height)"
46 | }
47 |
48 | func name() -> String {
49 | return pokemon.name
50 | }
51 |
52 | func imagePath() -> String? {
53 | return pokemon.spriteUrlString
54 | }
55 |
56 | func baseExperience() -> String {
57 | return "\(Constants.Translations.DetailScene.experience): \(pokemon.baseExperience)"
58 | }
59 |
60 | func date() -> String {
61 | let formatter = DateFormatter()
62 | formatter.dateFormat = "dd/mm/yyyy HH:MM"
63 | return formatter.string(from: pokemon.date)
64 | }
65 |
66 | func types() -> String {
67 | var allTypes: String = Constants.Translations.DetailScene.types + ": "
68 | for type in pokemon.types {
69 | allTypes.append(type.capitalized)
70 | allTypes.append(", ")
71 | }
72 | return allTypes
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Features/Detail/Sources/Scenes/Pokemon Detail Scene/PokemonDetailViewController.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 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Features/Detail/Sources/Scenes/Pokemon Detail Scene/PokemonDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PokemonDetailViewController.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 10/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Haneke
11 | import Common
12 |
13 | public class PokemonDetailViewController: UIViewController {
14 | var presenter: PokemonDetailPresenting?
15 |
16 | private var pokemonView: PokemonView?
17 |
18 | public override func viewDidLoad() {
19 | addPokemonView()
20 | }
21 |
22 | private func addPokemonView() {
23 | guard let pokemonView = PokemonView.loadFromNib() else { return }
24 |
25 | view.addSubview(pokemonView)
26 |
27 | pokemonView.center = view.center
28 | var point = pokemonView.center
29 | let height = pokemonView.frame.size.height
30 | point.y = height/2
31 | pokemonView.center = point
32 |
33 | self.pokemonView = pokemonView
34 | }
35 |
36 | public override func viewWillAppear(_ animated: Bool) {
37 | super.viewWillAppear(animated)
38 | configurePokemonView()
39 | }
40 |
41 | private func configurePokemonView() {
42 | guard let presenter = presenter else { return }
43 | guard let pokemonView = pokemonView else { return }
44 |
45 | pokemonView.weight.text = presenter.weight()
46 | pokemonView.height.text = presenter.height()
47 | pokemonView.name.text = ""
48 |
49 | pokemonView.experience.isHidden = false
50 | pokemonView.experience.text = presenter.baseExperience()
51 |
52 | pokemonView.date.isHidden = false
53 | pokemonView.date.text = presenter.date()
54 |
55 | pokemonView.types.isHidden = false
56 | pokemonView.types.text = presenter.types()
57 |
58 | title = presenter.name().capitalized
59 |
60 | guard let imagePath = presenter.imagePath() else { return }
61 | guard let imageURL = URL(string: imagePath) else { return }
62 | pokemonView.imageView.hnk_setImage(from: imageURL)
63 | }
64 | }
65 |
66 | extension PokemonDetailViewController: PokemonDetailView {
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/Features/Detail/Sources/Scenes/Pokemon Detail Scene/PokemonDetailWireframe.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PokemonDetailWireframe.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 10/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Common
11 |
12 | public class PokemonDetailWireframe {
13 |
14 | public static func makeViewController() -> PokemonDetailViewController {
15 | let storyboard = UIStoryboard.init(name: "PokemonDetailViewController", bundle: Bundle(for: PokemonDetailViewController.self))
16 | return PokemonDetailViewController.instantiateFromStoryboard(storyboard: storyboard)
17 | }
18 |
19 | public static func prepare(_ viewController: PokemonDetailViewController, pokemon: LocalPokemon) {
20 | let presenter = PokemonDetailPresenter(view: viewController, pokemon: pokemon)
21 | viewController.presenter = presenter
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Features/Detail/Tests/DetailTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailTests.swift
3 | // Detail
4 | //
5 | // Created by Ronan on 12/09/2021.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | @testable import Detail
10 |
11 | import XCTest
12 |
13 | class DetailTests: XCTestCase {
14 | override func setUpWithError() throws {
15 | super.setUp()
16 | }
17 |
18 | override func tearDownWithError() throws {
19 | super.tearDown()
20 | }
21 |
22 | func testDetail() {
23 | // Given
24 | // When
25 | // Then
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Features/Haneke/Example/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 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "PokeBallLogo_120.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "PokeBallLogo_180.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "1024x1024",
47 | "idiom" : "ios-marketing",
48 | "filename" : "PokeBallLogo_1024.png",
49 | "scale" : "1x"
50 | }
51 | ],
52 | "info" : {
53 | "version" : 1,
54 | "author" : "xcode"
55 | }
56 | }
--------------------------------------------------------------------------------
/Features/Haneke/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Haneke/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png
--------------------------------------------------------------------------------
/Features/Haneke/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Haneke/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png
--------------------------------------------------------------------------------
/Features/Haneke/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Haneke/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png
--------------------------------------------------------------------------------
/Features/Haneke/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Haneke/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png
--------------------------------------------------------------------------------
/Features/Haneke/Example/Resources/Assets.xcassets/Ball.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Ball.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Haneke/Example/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Features/Haneke/Example/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Haneke
3 |
4 | @main
5 | class AppDelegate: UIResponder, UIApplicationDelegate {
6 |
7 | var window: UIWindow?
8 |
9 | func application(
10 | _ application: UIApplication,
11 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
12 | ) -> Bool {
13 | window = UIWindow(frame: UIScreen.main.bounds)
14 | let viewController = makeViewController()
15 | addImageView(to: viewController.view)
16 | window?.rootViewController = viewController
17 | window?.makeKeyAndVisible()
18 | return true
19 | }
20 |
21 | func makeViewController() -> UIViewController {
22 | let viewController = UIViewController()
23 | viewController.view.backgroundColor = .white
24 | return viewController
25 | }
26 |
27 | func addImageView(to view: UIView) {
28 | let imagePath = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/196.png"
29 | let diameter: CGFloat = 128
30 | let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: diameter, height: diameter))
31 | view.addSubview(imageView)
32 |
33 | imageView.translatesAutoresizingMaskIntoConstraints = false
34 |
35 | NSLayoutConstraint.activate([
36 | imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
37 | imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
38 | imageView.widthAnchor.constraint(equalToConstant: diameter),
39 | imageView.heightAnchor.constraint(equalToConstant: diameter)
40 | ])
41 |
42 | if let imageURL = URL(string: imagePath) {
43 | imageView.hnk_setImage(from: imageURL)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Features/Haneke/Sources/HNKDiskFetcher.h:
--------------------------------------------------------------------------------
1 | //
2 | // HNKDiskFetcher.h
3 | // Haneke
4 | //
5 | // Created by Hermes Pique on 7/23/14.
6 | // Copyright (c) 2014 Hermes Pique. All rights reserved.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | #import
22 | #import "HNKCache.h"
23 |
24 | enum
25 | {
26 | HNKErrorDiskFetcherInvalidData = -500,
27 | };
28 |
29 | /**
30 | Fetcher that can provide a disk image. The key will be the given path.
31 | */
32 | @interface HNKDiskFetcher : NSObject
33 |
34 | /**
35 | Initializes a fetcher with the given path.
36 | @param path Image path.
37 | */
38 | - (instancetype)initWithPath:(NSString*)path NS_DESIGNATED_INITIALIZER;
39 |
40 | /**
41 | Cancels the current fetch. When a fetch is cancelled it should not call any of the provided blocks.
42 | @discussion This will be typically used by UI logic to cancel fetches during view reuse.
43 | */
44 | - (void)cancelFetch;
45 |
46 | - (instancetype)init NS_UNAVAILABLE;
47 |
48 | @end
49 |
--------------------------------------------------------------------------------
/Features/Haneke/Sources/HNKDiskFetcher.m:
--------------------------------------------------------------------------------
1 | //
2 | // HNKDiskFetcher.m
3 | // Haneke
4 | //
5 | // Created by Hermes Pique on 7/23/14.
6 | // Copyright (c) 2014 Hermes Pique. All rights reserved.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | #import "HNKDiskFetcher.h"
22 |
23 | @implementation HNKDiskFetcher {
24 | NSString *_path;
25 | BOOL _cancelled;
26 | }
27 |
28 | - (instancetype)initWithPath:(NSString*)path
29 | {
30 | if (self = [super init])
31 | {
32 | _path = path;
33 | }
34 | return self;
35 | }
36 |
37 | - (NSString*)key
38 | {
39 | return _path;
40 | }
41 |
42 | - (void)fetchImageWithSuccess:(void (^)(UIImage *image))successBlock failure:(void (^)(NSError *error))failureBlock;
43 | {
44 | _cancelled = NO;
45 | __weak __typeof__(self) weakSelf = self;
46 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
47 | __strong __typeof__(weakSelf) strongSelf = weakSelf;
48 |
49 | if (!strongSelf) return;
50 |
51 | if (strongSelf->_cancelled) return;
52 |
53 | NSString *path = strongSelf->_path;
54 |
55 | NSError *error = nil;
56 | NSData *data = [NSData dataWithContentsOfFile:path options:kNilOptions error:&error];
57 | if (!data)
58 | {
59 | HanekeLog(@"Request %@ failed with error %@", path, error);
60 | dispatch_async(dispatch_get_main_queue(), ^{
61 | failureBlock(error);
62 | });
63 | return;
64 | }
65 |
66 | if (strongSelf->_cancelled) return;
67 |
68 | UIImage *image = [UIImage imageWithData:data];
69 |
70 | if (!image)
71 | {
72 | NSString *errorDescription = [NSString stringWithFormat:NSLocalizedString(@"Failed to load image from data at path %@", @""), path];
73 | HanekeLog(@"%@", errorDescription);
74 | NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : errorDescription , NSFilePathErrorKey : path};
75 | NSError *error = [NSError errorWithDomain:HNKErrorDomain code:HNKErrorDiskFetcherInvalidData userInfo:userInfo];
76 | dispatch_async(dispatch_get_main_queue(), ^{
77 | failureBlock(error);
78 | });
79 | return;
80 | }
81 |
82 | dispatch_async(dispatch_get_main_queue(), ^{
83 | if (strongSelf->_cancelled) return;
84 |
85 | successBlock(image);
86 | });
87 | });
88 | }
89 |
90 | - (void)cancelFetch
91 | {
92 | _cancelled = YES;
93 | }
94 |
95 | - (void)dealloc
96 | {
97 | [self cancelFetch];
98 | }
99 |
100 | @end
101 |
--------------------------------------------------------------------------------
/Features/Haneke/Sources/HNKNetworkFetcher.h:
--------------------------------------------------------------------------------
1 | //
2 | // HNKNetworkFetcher.h
3 | // Haneke
4 | //
5 | // Created by Hermes Pique on 7/23/14.
6 | // Copyright (c) 2014 Hermes Pique. All rights reserved.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | #import
22 | #import "HNKCache.h"
23 |
24 | enum
25 | {
26 | HNKErrorNetworkFetcherInvalidData = -400,
27 | HNKErrorNetworkFetcherMissingData = -401,
28 | HNKErrorNetworkFetcherInvalidStatusCode = -402
29 | };
30 |
31 | /**
32 | Fetcher that can provide a network image. The key will be the absolute string of the given URL.
33 | */
34 | @interface HNKNetworkFetcher : NSObject
35 |
36 | /**
37 | Initializes a fetcher with the given URL.
38 | @param URL Image URL.
39 | */
40 | - (instancetype)initWithURL:(NSURL*)URL NS_DESIGNATED_INITIALIZER;
41 |
42 | - (instancetype)init NS_UNAVAILABLE;
43 |
44 | /**
45 | Image URL.
46 | */
47 | @property (nonatomic, readonly) NSURL *URL;
48 |
49 |
50 | /**
51 | Cancels the current fetch. When a fetch is cancelled it should not call any of the provided blocks.
52 | @discussion This will be typically used by UI logic to cancel fetches during view reuse.
53 | */
54 | - (void)cancelFetch;
55 |
56 | @end
57 |
58 | @interface HNKNetworkFetcher (Subclassing)
59 |
60 | /**
61 | Returns the URL sessions used to download the image. Override to use a custom session. Uses sharedSession by default.
62 | */
63 | @property (nonatomic, readonly) NSURLSession *URLSession;
64 |
65 | @end
66 |
--------------------------------------------------------------------------------
/Features/Haneke/Sources/HNKSimpleFetcher.h:
--------------------------------------------------------------------------------
1 | //
2 | // HNKSimpleFetcher.h
3 | // Haneke
4 | //
5 | // Created by Hermes Pique on 8/19/14.
6 | // Copyright (c) 2014 Hermes Pique. All rights reserved.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | #import
22 | #import "HNKCache.h"
23 |
24 | /**
25 | Simple fetcher that represents a key-image pair.
26 | @discussion Used as a convenience by the UIKit categories.
27 | */
28 | @interface HNKSimpleFetcher : NSObject
29 |
30 | /**
31 | Initializes a fetcher with the given key and image.
32 | @param key Image key.
33 | @param image Image that will be returned by the fetcher.
34 | */
35 | - (instancetype)initWithKey:(NSString*)key image:(UIImage*)image NS_DESIGNATED_INITIALIZER;
36 |
37 | - (instancetype)init NS_UNAVAILABLE;
38 |
39 | @end
40 |
--------------------------------------------------------------------------------
/Features/Haneke/Sources/HNKSimpleFetcher.m:
--------------------------------------------------------------------------------
1 | //
2 | // HNKSimpleFetcher.m
3 | // Haneke
4 | //
5 | // Created by Hermes Pique on 8/19/14.
6 | // Copyright (c) 2014 Hermes Pique. All rights reserved.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | #import "HNKSimpleFetcher.h"
22 |
23 | @implementation HNKSimpleFetcher {
24 | NSString *_key;
25 | UIImage *_image;
26 | }
27 |
28 | - (instancetype)initWithKey:(NSString*)key image:(UIImage*)image
29 | {
30 | if (self = [super init])
31 | {
32 | _key = [key copy];
33 | _image = image;
34 | }
35 | return self;
36 | }
37 |
38 | - (NSString*)key
39 | {
40 | return _key;
41 | }
42 |
43 | - (void)fetchImageWithSuccess:(void (^)(UIImage *image))successBlock failure:(void (^)(NSError *error))failureBlock;
44 | {
45 | successBlock(_image);
46 | }
47 |
48 | @end
49 |
--------------------------------------------------------------------------------
/Features/Haneke/Sources/Haneke.h:
--------------------------------------------------------------------------------
1 | //
2 | // Haneke.h
3 | // Haneke
4 | //
5 | // Created by Hermés Piqué on 13/03/14.
6 | // Copyright (c) 2014 Hermes Pique. All rights reserved.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | #import "HNKCache.h"
22 | #import "HNKDiskFetcher.h"
23 | #import "HNKNetworkFetcher.h"
24 | #import "UIImageView+Haneke.h"
25 | #import "UIButton+Haneke.h"
26 | #import "HNKDiskCache.h"
27 | #import "HNKSimpleFetcher.h"
28 | #import "UIView+Haneke.h"
29 |
--------------------------------------------------------------------------------
/Features/Haneke/Sources/UIView+Haneke.h:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+Haneke.h
3 | // Haneke
4 | //
5 | // Created by Hermes Pique on 8/20/14.
6 | // Copyright (c) 2014 Hermes Pique. All rights reserved.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | #import
22 | #import "HNKCache.h"
23 |
24 | extern const CGFloat HNKViewFormatCompressionQuality;
25 | extern const unsigned long long HNKViewFormatDiskCapacity;
26 | extern const NSTimeInterval HNKViewSetImageAnimationDuration;
27 |
28 | /**
29 | Convenience category used in the other UIKit categories to avoid repeating code. Intended for internal use.
30 | */
31 | @interface UIView (Haneke)
32 |
33 | @property (nonatomic, readonly) HNKScaleMode hnk_scaleMode;
34 |
35 | @end
36 |
37 | @interface HNKCache(UIView)
38 |
39 | + (void)registerSharedFormat:(HNKCacheFormat*)format;
40 |
41 | + (HNKCacheFormat*)sharedFormatWithSize:(CGSize)size scaleMode:(HNKScaleMode)scaleMode;
42 |
43 | @end
44 |
--------------------------------------------------------------------------------
/Features/Haneke/Sources/UIView+Haneke.m:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+Haneke.m
3 | // Haneke
4 | //
5 | // Created by Hermes Pique on 8/20/14.
6 | // Copyright (c) 2014 Hermes Pique. All rights reserved.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | #import "UIView+Haneke.h"
22 | #import "HNKCache.h"
23 | #import
24 |
25 | const CGFloat HNKViewFormatCompressionQuality = 0.75;
26 | const unsigned long long HNKViewFormatDiskCapacity = 50 * 1024 * 1024;
27 | const NSTimeInterval HNKViewSetImageAnimationDuration = 0.1;
28 |
29 | static NSString *NSStringFromHNKScaleMode(HNKScaleMode scaleMode)
30 | {
31 | switch (scaleMode) {
32 | case HNKScaleModeFill:
33 | return @"fill";
34 | case HNKScaleModeAspectFill:
35 | return @"aspectfill";
36 | case HNKScaleModeAspectFit:
37 | return @"aspectfit";
38 | case HNKScaleModeNone:
39 | return @"scalenone";
40 | }
41 | return nil;
42 | }
43 |
44 | @implementation UIView (Haneke)
45 |
46 | - (HNKScaleMode)hnk_scaleMode
47 | {
48 | switch (self.contentMode) {
49 | case UIViewContentModeScaleToFill:
50 | return HNKScaleModeFill;
51 | case UIViewContentModeScaleAspectFit:
52 | return HNKScaleModeAspectFit;
53 | case UIViewContentModeScaleAspectFill:
54 | return HNKScaleModeAspectFill;
55 | case UIViewContentModeRedraw:
56 | case UIViewContentModeCenter:
57 | case UIViewContentModeTop:
58 | case UIViewContentModeBottom:
59 | case UIViewContentModeLeft:
60 | case UIViewContentModeRight:
61 | case UIViewContentModeTopLeft:
62 | case UIViewContentModeTopRight:
63 | case UIViewContentModeBottomLeft:
64 | case UIViewContentModeBottomRight:
65 | return HNKScaleModeNone;
66 | }
67 | }
68 |
69 | @end
70 |
71 | @implementation HNKCache(UIView)
72 |
73 | + (void)registerSharedFormat:(HNKCacheFormat*)format
74 | {
75 | HNKCache *cache = [HNKCache sharedCache];
76 | if (cache.formats[format.name] != format)
77 | {
78 | [[HNKCache sharedCache] registerFormat:format];
79 | }
80 | }
81 |
82 | + (HNKCacheFormat*)sharedFormatWithSize:(CGSize)size scaleMode:(HNKScaleMode)scaleMode
83 | {
84 | NSString *scaleModeName = NSStringFromHNKScaleMode(scaleMode);
85 | NSString *name = [NSString stringWithFormat:@"auto-%ldx%ld-%@", (long)size.width, (long)size.height, scaleModeName];
86 | HNKCache *cache = [HNKCache sharedCache];
87 | HNKCacheFormat *format = cache.formats[name];
88 | if (!format)
89 | {
90 | format = [[HNKCacheFormat alloc] initWithName:name];
91 | format.size = size;
92 | format.diskCapacity = HNKViewFormatDiskCapacity;
93 | format.allowUpscaling = YES;
94 | format.compressionQuality = HNKViewFormatCompressionQuality;
95 | format.scaleMode = scaleMode;
96 | [cache registerFormat:format];
97 | }
98 | return format;
99 | }
100 |
101 | @end
102 |
--------------------------------------------------------------------------------
/Features/Haneke/Tests/HanekeKitTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 |
4 | final class HanekeKitTests: XCTestCase {
5 | func test_example() {
6 | XCTAssertEqual("HanekeKit", "HanekeKit")
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Features/Home/Example/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 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "PokeBallLogo_120.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "PokeBallLogo_180.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "1024x1024",
47 | "idiom" : "ios-marketing",
48 | "filename" : "PokeBallLogo_1024.png",
49 | "scale" : "1x"
50 | }
51 | ],
52 | "info" : {
53 | "version" : 1,
54 | "author" : "xcode"
55 | }
56 | }
--------------------------------------------------------------------------------
/Features/Home/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Home/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png
--------------------------------------------------------------------------------
/Features/Home/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Home/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png
--------------------------------------------------------------------------------
/Features/Home/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Home/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png
--------------------------------------------------------------------------------
/Features/Home/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Home/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png
--------------------------------------------------------------------------------
/Features/Home/Example/Resources/Assets.xcassets/Ball.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Ball.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Home/Example/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Features/Home/Example/Resources/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 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/Features/Home/Example/Sources/AppController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppController.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | protocol AppControlling {
13 | func start()
14 | }
15 |
16 | class AppController: AppControlling {
17 | var coordinator: Coordinating?
18 |
19 | func start() {
20 | let dataProvider = DataProvider()
21 |
22 | if Configuration.uiTesting == true {
23 | let storage = FileStorage()
24 | storage.remove(AppData.pokemonFile, from: dataProvider.appData.directory())
25 | }
26 |
27 | dataProvider.start()
28 |
29 | coordinator = Coordinator()
30 | coordinator?.dataProvider = dataProvider
31 | coordinator?.start()
32 |
33 | dataProvider.notifier = coordinator as? Notifier
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Features/Home/Example/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import HomeUI
3 |
4 | @main
5 | class AppDelegate: UIResponder, UIApplicationDelegate {
6 |
7 | private let appController = AppController()
8 |
9 | func application(
10 | _ application: UIApplication,
11 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
12 | ) -> Bool {
13 |
14 | appController.start()
15 |
16 | return true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Features/Home/Resources/Assets.xcassets/Backpack.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "PokemonTrainerBackpack.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Home/Resources/Assets.xcassets/Backpack.imageset/PokemonTrainerBackpack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Home/Resources/Assets.xcassets/Backpack.imageset/PokemonTrainerBackpack.png
--------------------------------------------------------------------------------
/Features/Home/Resources/Assets.xcassets/Ball.imageset/Ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Home/Resources/Assets.xcassets/Ball.imageset/Ball.png
--------------------------------------------------------------------------------
/Features/Home/Resources/Assets.xcassets/Ball.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Ball.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Home/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Features/Home/Sources/Scenes/Home Scene/HomeActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeActions.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Common
10 |
11 | public protocol HomeActions {
12 | func ballButtonAction()
13 | func backpackButtonAction()
14 | }
15 |
16 | extension Actions: HomeActions {
17 | public func ballButtonAction() {
18 | coordinator.showCatchScene()
19 | }
20 |
21 | public func backpackButtonAction() {
22 | coordinator.showBackpackScene()
23 | }
24 | }
25 |
26 | import SwiftUI
27 |
28 | public protocol HomeUIScenes {
29 | func mainView() -> AnyView
30 | }
31 |
--------------------------------------------------------------------------------
/Features/Home/Sources/Scenes/Home Scene/HomeDataProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeDataProvider.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Common
10 |
11 | public protocol HomeDataProvider {
12 |
13 | }
14 |
15 | extension DataProvider: HomeDataProvider {
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Features/Home/Sources/Scenes/Home Scene/HomePresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomePresenter.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | public protocol HomeView: AnyObject {
10 |
11 | }
12 |
13 | public protocol HomePresenting: AnyObject {
14 | func ballButtonAction()
15 | func backpackButtonAction()
16 | }
17 |
18 | public class HomePresenter: HomePresenting {
19 |
20 | // MARK: Properties
21 |
22 | private weak var view: HomeView?
23 | private var actions: HomeActions
24 | private var dataProvider: HomeDataProvider
25 |
26 | // MARK: Typealias
27 |
28 | typealias Actions = HomeActions
29 | typealias DataProvider = HomeDataProvider
30 | typealias View = HomeView
31 |
32 | required init(view: HomeView, actions: HomeActions, dataProvider: HomeDataProvider) {
33 | self.view = view
34 | self.actions = actions
35 | self.dataProvider = dataProvider
36 | }
37 |
38 | public func ballButtonAction() {
39 | actions.ballButtonAction()
40 | }
41 |
42 | public func backpackButtonAction() {
43 | actions.backpackButtonAction()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Features/Home/Sources/Scenes/Home Scene/HomeViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewController.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public class HomeViewController: UIViewController {
12 | var presenter: HomePresenting?
13 |
14 | @IBAction func ballButtonAction() {
15 | guard let presenter = presenter else { return }
16 | presenter.ballButtonAction()
17 | }
18 |
19 | @IBAction func backpackButtonAction() {
20 | guard let presenter = presenter else { return }
21 | presenter.backpackButtonAction()
22 | }
23 | }
24 |
25 | extension HomeViewController: HomeView {
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Features/Home/Sources/Scenes/Home Scene/HomeWireframe.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeWireframe.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public class HomeWireframe {
12 |
13 | public static func makeViewController() -> HomeViewController {
14 | let storyboard = UIStoryboard.init(name: "HomeViewController", bundle: Bundle(for: HomeViewController.self))
15 | return HomeViewController.instantiateFromStoryboard(storyboard: storyboard)
16 | }
17 |
18 | public static func prepare(_ viewController: HomeViewController, actions: HomeActions, dataProvider: HomeDataProvider) {
19 | let presenter = HomePresenter(view: viewController, actions: actions, dataProvider: dataProvider)
20 | viewController.presenter = presenter
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Features/Home/Tests/HomeUITests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 |
4 | final class HomeUITests: XCTestCase {
5 | func test_example() {
6 | XCTAssertEqual("HomeUI", "HomeUI")
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Features/Network/Example/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 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "PokeBallLogo_120.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "PokeBallLogo_180.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "1024x1024",
47 | "idiom" : "ios-marketing",
48 | "filename" : "PokeBallLogo_1024.png",
49 | "scale" : "1x"
50 | }
51 | ],
52 | "info" : {
53 | "version" : 1,
54 | "author" : "xcode"
55 | }
56 | }
--------------------------------------------------------------------------------
/Features/Network/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Network/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png
--------------------------------------------------------------------------------
/Features/Network/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Network/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png
--------------------------------------------------------------------------------
/Features/Network/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Network/Example/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png
--------------------------------------------------------------------------------
/Features/Network/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Network/Example/Resources/Assets.xcassets/Ball.imageset/Ball.png
--------------------------------------------------------------------------------
/Features/Network/Example/Resources/Assets.xcassets/Ball.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Ball.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Network/Example/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Features/Network/Example/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import NetworkKit
3 | import Common
4 |
5 | @main
6 | class AppDelegate: UIResponder, UIApplicationDelegate {
7 |
8 | var window: UIWindow?
9 |
10 | func application(
11 | _ application: UIApplication,
12 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
13 | ) -> Bool {
14 | let service = PokemonSearchService()
15 | let dataProvider = DataProvider()
16 | let viewController = SimpleViewController(dataProvider: dataProvider, networkService: service)
17 | let navigationController = UINavigationController(rootViewController: viewController)
18 |
19 | window = UIWindow(frame: UIScreen.main.bounds)
20 | window?.rootViewController = navigationController
21 | window?.makeKeyAndVisible()
22 |
23 | return true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Features/Network/Example/Sources/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // NetworkKitExample
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // swiftlint:disable nesting identifier_name
12 |
13 | struct Constants {
14 |
15 | struct Network {
16 | static let baseUrlPath = "https://pokeapi.co/api/v2/"
17 | static let searchPath = "pokemon"
18 | }
19 |
20 | struct Image {
21 | static let pokemonPlaceholder = "PokemonPlaceholder"
22 | }
23 |
24 | struct Translations {
25 | static let loading = "Loading"
26 | static let ok = "OK"
27 | static let cancel = "Cancel"
28 |
29 | struct HomeScene {
30 | static let catchTitle = "Catch a Pokemon"
31 | }
32 |
33 | struct CatchScene {
34 | static let weight = "WEIGHT"
35 | static let height = "HEIGHT"
36 | static let leaveOrCatchAlertMessageTitle = "Do you want to leave it or catch it?"
37 | static let leaveItButtonTitle = "Leave it"
38 | static let catchItButtonTitle = "Catch it!"
39 | static let alreadyHaveItAlertMessageTitle = "You have already caught one of this species, you'll have to leave this one..."
40 |
41 | static let noPokemonFoundAlertTitle = "No Pokemon found, you will have to try again."
42 |
43 | }
44 |
45 | struct SimpleView {
46 |
47 | static let title = "NetworkKit Example"
48 | static let labelText = "Choose a Pokemon"
49 |
50 | static let placeholder = "A number between 1 and 1000"
51 |
52 | struct Button {
53 | static let search = "Search"
54 | }
55 |
56 | struct Alert {
57 |
58 | static let found = "Pokemon found: "
59 |
60 | struct Error {
61 | static let enterVlidNumber = "Enter valid number"
62 | static let outOfRange = "Number out of range"
63 | static let nameNotFound = "Unknown"
64 | }
65 |
66 | }
67 | }
68 |
69 | struct Error {
70 | static let jsonDecodingError = "Error: JSON decoding error."
71 | static let noDataError = "Error: No data received."
72 | static let noResultsFound = "No results were found for your search."
73 | static let statusCode404 = "404 Not found"
74 | }
75 | }
76 |
77 | struct PokemonAPI {
78 | static let minIdentifier = 1
79 | static let maxIdentifier = 1000
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Features/Network/Example/Sources/DataProviderExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataProviderExtension.swift
3 | // NetworkKitExample
4 | //
5 | // Created by Ronan O Ciosig on 5/6/21.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import NetworkKit
11 | import os.log
12 | import Common
13 | import Combine
14 |
15 | protocol DataSearchProviding {
16 | func search(identifier: Int, networkService: SearchService)
17 | func pokemonName() -> String?
18 | }
19 |
20 | extension DataProvider: DataSearchProviding {
21 | public func search(identifier: Int, networkService: SearchService) {
22 | appData.pokemon = nil
23 | let queue = DispatchQueue.main
24 |
25 | searchCancellable = networkService.search(identifier: identifier)
26 | .receive(on: queue)
27 | .sink(receiveCompletion: { completion in
28 | switch completion {
29 | case .failure(let error):
30 | let errorMessage = "\(error.localizedDescription)"
31 | os_log("Error: %s", log: Log.data, type: .error, errorMessage)
32 | self.notifier?.dataReceived(errorMessage: errorMessage, on: queue)
33 | case .finished:
34 | print("All good")
35 | }
36 |
37 | }, receiveValue: { data in
38 | do {
39 | let decoder = JSONDecoder()
40 | decoder.keyDecodingStrategy = .convertFromSnakeCase
41 | let pokemon = try decoder.decode(Pokemon.self, from: data)
42 | self.appData.pokemon = pokemon
43 | self.notifier?.dataReceived(errorMessage: nil, on: queue)
44 | os_log("Success: %s", log: Log.network, type: .default, "Loaded")
45 | } catch {
46 | let errorMessage = "\(error.localizedDescription)"
47 | os_log("Error: %s", log: Log.data, type: .error, errorMessage)
48 | self.notifier?.dataReceived(errorMessage: errorMessage, on: queue)
49 | }
50 | })
51 | }
52 |
53 | func pokemonName() -> String? {
54 | return appData.pokemon?.name
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Features/Network/Example/Sources/Log.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Log.swift
3 | // NetworkKitExample
4 | //
5 | // Created by Ronan O Ciosig on 9/5/21.
6 | // Copyright © 2021 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 |
12 | struct Log {
13 | static var general = OSLog(subsystem: "com.sonomos.pokedex", category: "general")
14 | static var network = OSLog(subsystem: "com.sonomos.pokedex", category: "network")
15 | static var data = OSLog(subsystem: "com.sonomos.pokedex", category: "data")
16 | }
17 |
--------------------------------------------------------------------------------
/Features/Network/Sources/Core/Configuration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Configuration.swift
3 | // NetworkKit
4 | //
5 | // Created by Ronan on 09/02/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | struct Configuration {
12 |
13 | static var uiTesting: Bool {
14 | let arguments = ProcessInfo.processInfo.arguments
15 | return arguments.contains("UITesting")
16 | }
17 |
18 | static var networkTesting: Bool {
19 | return CommandLine.arguments.contains("NetworkTesting")
20 | }
21 |
22 | static var searchErrorTesting: Bool {
23 | return CommandLine.arguments.contains("Error_401")
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Features/Network/Sources/Core/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // NetworkKit
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // swiftlint:disable nesting identifier_name
12 |
13 | struct Constants {
14 |
15 | struct Network {
16 | static let baseUrlPath = "https://pokeapi.co/api/v2/"
17 | static let searchPath = "pokemon"
18 | }
19 |
20 | struct Image {
21 | static let pokemonPlaceholder = "PokemonPlaceholder"
22 | }
23 |
24 | struct Translations {
25 | static let loading = "Loading"
26 | static let ok = "OK"
27 | static let cancel = "Cancel"
28 |
29 | struct HomeScene {
30 | static let catchTitle = "Catch a Pokemon"
31 | }
32 |
33 | struct CatchScene {
34 | static let weight = "WEIGHT"
35 | static let height = "HEIGHT"
36 | static let leaveOrCatchAlertMessageTitle = "Do you want to leave it or catch it?"
37 | static let leaveItButtonTitle = "Leave it"
38 | static let catchItButtonTitle = "Catch it!"
39 | static let alreadyHaveItAlertMessageTitle = "You have already caught one of this species, you'll have to leave this one..."
40 |
41 | static let noPokemonFoundAlertTitle = "No Pokemon found, you will have to try again."
42 |
43 | }
44 |
45 | struct BackpackScene {
46 | static let title = "Backpack"
47 | static let closeButton = "Close"
48 | }
49 |
50 | struct DetailScene {
51 | static let weight = "Weight"
52 | static let height = "Height"
53 | static let date = "Date"
54 | static let experience = "Experience"
55 | static let types = "Types"
56 | }
57 |
58 | struct Error {
59 | static let jsonDecodingError = "Error: JSON decoding error."
60 | static let noDataError = "Error: No data received."
61 | static let noResultsFound = "No results were found for your search."
62 | static let statusCode404 = "404"
63 | static let notFound = "Error 401 Pokemon not found"
64 | }
65 | }
66 |
67 | struct PokemonAPI {
68 | static let minIdentifier = 1
69 | static let maxIdentifier = 1000
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Features/Network/Sources/Mock/MockData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockData.swift
3 | // NetworkKit
4 | //
5 | // Created by Ronan on 23/11/2018.
6 | // Copyright © 2018 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // swiftlint:disable all
12 |
13 | class MockData {
14 | static let fileType = "json"
15 | static let fileReadError = "File not readable"
16 | static let fileNotFoundError = "File not found"
17 |
18 | static func load(name: String) throws -> Data? {
19 | let bundle = Bundle.init(for: MockData.self)
20 |
21 | if let path = bundle.path(forResource: name, ofType: fileType) {
22 | let fileUrl = URL.init(fileURLWithPath: path)
23 | do {
24 | let data = try Data.init(contentsOf: fileUrl)
25 | return data
26 | } catch {
27 | let error = fileReadError as! Error
28 | throw error
29 | }
30 | } else {
31 | let error = fileNotFoundError as! Error
32 | throw error
33 | }
34 | }
35 |
36 | static func loadNoResultsResponse() throws -> Data? {
37 | return try load(name: "noResultsServerResponse" )
38 | }
39 |
40 | static func loadResponse() throws -> Data? {
41 | return try load(name: "Pokemon5")
42 | }
43 |
44 | static func loadOtherResponse() throws -> Data? {
45 | return try load(name: "Pokemon12")
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Features/Network/Sources/Services/PokemonSearchEndpoint+FactoryMethods.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PokemonSearchEndpoint+FactoryMethods.swift
3 | // NetworkKit
4 | //
5 | // Created by Ronan on 01/01/2022.
6 | // Copyright © 2022 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension PokemonSearchEndpoint {
12 | public func makeURL() -> URL {
13 | let urlString = baseURL.absoluteString + path
14 | guard let url = URL(string: urlString) else {
15 | fatalError("Failed to create URL for endpoint: \(urlString)")
16 | }
17 | return url
18 | }
19 |
20 | public func makeURLRequest() -> URLRequest {
21 | let url = makeURL()
22 | var request = URLRequest(url: url)
23 | request.httpMethod = method
24 | request.allHTTPHeaderFields = headers
25 | return request
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Features/Network/Sources/Services/PokemonSearchEndpoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PokemonSearchEndpoint.swift
3 | // NetworkKit
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum PokemonSearchEndpoint {
12 | case search(identifier: Int)
13 | }
14 |
15 | extension PokemonSearchEndpoint {
16 |
17 | public var baseURL: URL {
18 | // swiftlint:disable force_unwrapping
19 | return URL(string: Constants.Network.baseUrlPath)!
20 | // swiftlint:enable force_unwrapping
21 | }
22 |
23 | public var path: String {
24 | switch self {
25 | case .search(let identifier):
26 | return Constants.Network.searchPath + "/\(identifier)/"
27 | }
28 | }
29 |
30 | public var method: String {
31 | return "GET"
32 | }
33 |
34 | public var sampleData: Data {
35 | // swiftlint:disable force_try force_unwrapping
36 | return try! MockData.loadResponse()!
37 | // swiftlint:enable force_try force_unwrapping
38 | }
39 |
40 | public var headers: [String: String]? {
41 | return nil
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Features/Network/Tests/Mock/MockData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockData.swift
3 | // NetworkKit
4 | //
5 | // Created by Ronan on 23/11/2018.
6 | // Copyright © 2018 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // swiftlint:disable all
12 |
13 | class MockData {
14 | static let fileType = "json"
15 | static let fileReadError = "File not readable"
16 | static let fileNotFoundError = "File not found"
17 |
18 | static func load(name: String) throws -> Data? {
19 | let bundle = Bundle.init(for: MockData.self)
20 |
21 | if let path = bundle.path(forResource: name, ofType: fileType) {
22 | let fileUrl = URL.init(fileURLWithPath: path)
23 | do {
24 | let data = try Data.init(contentsOf: fileUrl)
25 | return data
26 | } catch {
27 | let error = fileReadError as! Error
28 | throw error
29 | }
30 | } else {
31 | let error = fileNotFoundError as! Error
32 | throw error
33 | }
34 | }
35 |
36 | static func loadNoResultsResponse() throws -> Data? {
37 | return try load(name: "noResultsServerResponse" )
38 | }
39 |
40 | static func loadResponse() throws -> Data? {
41 | return try load(name: "Pokemon5")
42 | }
43 |
44 | static func loadOtherResponse() throws -> Data? {
45 | return try load(name: "Pokemon12")
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Features/Network/Tests/MockSessionFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockSessionFactory.swift
3 | // NetworkKitTests
4 | //
5 | // Created by Ronan on 03/01/2022.
6 | // Copyright © 2022 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct MockSessionFactory {
12 | static func make(url: URL, data: Data, statusCode: Int) -> URLSession {
13 |
14 | guard let response = HTTPURLResponse(url: url,
15 | statusCode: statusCode,
16 | httpVersion: nil,
17 | headerFields: nil) else {
18 | return URLSession.shared
19 | }
20 |
21 | let mockResponse = MockResponse(response: response, url: url, data: data)
22 |
23 | URLProtocolMock.testResponses = [url: mockResponse]
24 |
25 | // now setup a configuration to use our mock
26 | let config = URLSessionConfiguration.ephemeral
27 | config.protocolClasses = [URLProtocolMock.self]
28 |
29 | // and create the URLSession form that
30 | return URLSession(configuration: config)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Features/Network/Tests/URLProtocolMock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLProtocolMock.swift
3 | // NetworkKitTests
4 | //
5 | // Created by Ronan on 02/01/2022.
6 | // Copyright © 2022 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct MockResponse {
12 | let response: URLResponse
13 | let url: URL
14 | let data: Data?
15 | }
16 |
17 | class URLProtocolMock: URLProtocol {
18 | // this dictionary maps URLs to mock responses containing data
19 | static var testResponses = [URL: MockResponse]()
20 | static var error: Error?
21 |
22 | // say we want to handle all types of request
23 | override class func canInit(with request: URLRequest) -> Bool {
24 | return true
25 | }
26 |
27 | // ignore this method; just send back what we were given
28 | override class func canonicalRequest(for request: URLRequest) -> URLRequest {
29 | return request
30 | }
31 |
32 | override func startLoading() {
33 | if let url = request.url {
34 |
35 | // if we have a valid URL with a response…
36 | if let response = URLProtocolMock.testResponses[url] {
37 |
38 | // …and if we have test data for that URL…
39 | if url.absoluteString == response.url.absoluteString, let data = response.data {
40 | self.client?.urlProtocol(self, didLoad: data)
41 | }
42 |
43 | // …and we return our response if defined…
44 | self.client?.urlProtocol(self,
45 | didReceive: response.response,
46 | cacheStoragePolicy: .notAllowed)
47 | }
48 | }
49 |
50 | // …and we return our error if defined…
51 | if let error = URLProtocolMock.error {
52 | self.client?.urlProtocol(self, didFailWithError: error)
53 | }
54 |
55 | // mark that we've finished
56 | self.client?.urlProtocolDidFinishLoading(self)
57 | }
58 |
59 | // this method is required but doesn't need to do anything
60 | override func stopLoading() {
61 |
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Features/Pokedex/.tuist-version:
--------------------------------------------------------------------------------
1 | 1.36.0
2 |
--------------------------------------------------------------------------------
/Features/Pokedex/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 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "PokeBallLogo_120.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "PokeBallLogo_180.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "1024x1024",
47 | "idiom" : "ios-marketing",
48 | "filename" : "PokeBallLogo_1024.png",
49 | "scale" : "1x"
50 | }
51 | ],
52 | "info" : {
53 | "version" : 1,
54 | "author" : "xcode"
55 | }
56 | }
--------------------------------------------------------------------------------
/Features/Pokedex/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Pokedex/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png
--------------------------------------------------------------------------------
/Features/Pokedex/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Pokedex/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png
--------------------------------------------------------------------------------
/Features/Pokedex/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Pokedex/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png
--------------------------------------------------------------------------------
/Features/Pokedex/Resources/Assets.xcassets/Ball.imageset/Ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Pokedex/Resources/Assets.xcassets/Ball.imageset/Ball.png
--------------------------------------------------------------------------------
/Features/Pokedex/Resources/Assets.xcassets/Ball.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Ball.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Features/Pokedex/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Features/Pokedex/Sources/Core/AppController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppController.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | protocol AppControlling {
13 | func start()
14 | }
15 |
16 | class AppController: AppControlling {
17 | var coordinator: Coordinating?
18 |
19 | func start() {
20 | let dataProvider = DataProvider()
21 |
22 | if Configuration.uiTesting == true {
23 | let storage = FileStorage()
24 | storage.remove(AppData.pokemonFile, from: dataProvider.appData.directory())
25 | }
26 |
27 | dataProvider.start()
28 |
29 | coordinator = Coordinator()
30 | coordinator?.dataProvider = dataProvider
31 | coordinator?.start()
32 |
33 | dataProvider.notifier = coordinator as? Notifier
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Features/Pokedex/Sources/Core/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Pokedex
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | private let appController = AppController()
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 |
18 | disableAnimations()
19 |
20 | appController.start()
21 |
22 | return true
23 | }
24 |
25 | func disableAnimations() {
26 | let arguments = ProcessInfo.processInfo.arguments
27 | UIView.setAnimationsEnabled(!arguments.contains("UITesting"))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Features/Pokedex/Sources/Core/DataProviderExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataProviderExtension.swift
3 | // PokedexCommon
4 | //
5 | // Created by Ronan O Ciosig on 5/6/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import NetworkKit
11 | import os.log
12 | import Common
13 | import Combine
14 |
15 | public protocol DataSearchProviding {
16 | func search(identifier: Int, networkService: SearchService, queue: DispatchQueue)
17 | }
18 |
19 | extension DataProvider: DataSearchProviding {
20 | public func search(identifier: Int,
21 | networkService: SearchService,
22 | queue: DispatchQueue = DispatchQueue.main) {
23 | appData.pokemon = nil
24 |
25 | if Configuration.asyncTesting {
26 | Task {
27 | do {
28 | guard let data = try await networkService.search(identifier: identifier) else {
29 | show(errorMessage: Constants.Translations.Error.asyncError, on: queue)
30 | return
31 | }
32 |
33 | decode(data: data, on: queue)
34 | } catch {
35 | show(errorMessage: Constants.Translations.Error.asyncError, on: queue)
36 | }
37 | }
38 | return
39 | }
40 |
41 | searchCancellable = networkService.search(identifier: identifier)
42 | .receive(on: queue)
43 | .sink(receiveCompletion: { completion in
44 | switch completion {
45 | case .failure(let error):
46 | let errorMessage = "\(error.localizedDescription)"
47 | os_log("Error: %s", log: Log.data, type: .error, errorMessage)
48 | self.notifier?.dataReceived(errorMessage: errorMessage, on: queue)
49 | case .finished:
50 | print("All good")
51 | }
52 |
53 | }, receiveValue: { [weak self] data in
54 | self?.decode(data: data, on: queue)
55 | })
56 | }
57 |
58 | func decode(data: Data, on queue: DispatchQueue) {
59 | do {
60 | let decoder = JSONDecoder()
61 | decoder.keyDecodingStrategy = .convertFromSnakeCase
62 | let pokemon = try decoder.decode(Pokemon.self, from: data)
63 | self.appData.pokemon = pokemon
64 | self.notifier?.dataReceived(errorMessage: nil, on: queue)
65 | os_log("Success: %s", log: Log.network, type: .default, "Loaded")
66 | } catch {
67 | let errorMessage = "\(error.localizedDescription)"
68 | show(errorMessage: errorMessage, on: queue)
69 | }
70 | }
71 |
72 | func show(errorMessage: String, on queue: DispatchQueue) {
73 | os_log("Error: %s", log: Log.data, type: .error, errorMessage)
74 | self.notifier?.dataReceived(errorMessage: errorMessage, on: queue)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Features/Pokedex/Sources/Core/Log.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Log.swift
3 | // PokedexCommon
4 | //
5 | // Created by Ronan O Ciosig on 5/6/21.
6 | // Copyright © 2021 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 |
12 | public struct Log {
13 | public static var general = OSLog(subsystem: "com.sonomos.pokedex", category: "general")
14 | public static var network = OSLog(subsystem: "com.sonomos.pokedex", category: "network")
15 | public static var data = OSLog(subsystem: "com.sonomos.pokedex", category: "data")
16 | }
17 |
--------------------------------------------------------------------------------
/Features/Pokedex/Tests/AppDataTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDataTests.swift
3 | // PokedexTests
4 | //
5 | // Created by Ronan on 10/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | // swiftlint:disable all
10 |
11 | import XCTest
12 | import Pokedex
13 | import Common
14 |
15 | //@testable import Pokedex
16 |
17 | class AppDataTests: XCTestCase {
18 | enum PokemonId: Int {
19 | case pokemon5
20 | case pokemon12
21 | }
22 |
23 | func testNewSpecies() {
24 | let appData = AppData(storage: FileStorage())
25 | let pokemon5 = loadPokemon(identifier: .pokemon5)
26 | let pokemon12 = loadPokemon(identifier: .pokemon12)
27 |
28 | appData.pokemon = pokemon5
29 | let localPokemon = PokemonParser.parse(pokemon: pokemon5)
30 | appData.pokemons.append(localPokemon)
31 |
32 | var newSpecies = appData.newSpecies()
33 |
34 | XCTAssertFalse(newSpecies)
35 |
36 | appData.pokemon = pokemon12
37 |
38 | newSpecies = appData.newSpecies()
39 |
40 | XCTAssertTrue(newSpecies)
41 | }
42 |
43 | func loadPokemon(identifier: PokemonId) -> Pokemon {
44 | let data: Data
45 |
46 | switch identifier {
47 | case .pokemon5:
48 | data = try! MockData.loadResponse()!
49 | case .pokemon12:
50 | data = try! MockData.loadOtherResponse()!
51 | }
52 |
53 | let decoder = JSONDecoder()
54 | decoder.keyDecodingStrategy = .convertFromSnakeCase
55 | let pokemon = try! decoder.decode(Pokemon.self, from: data)
56 | return pokemon
57 | }
58 |
59 | func testSortByOrder() {
60 | let appData = AppData(storage: FileStorage())
61 | let pokemon5 = loadPokemon(identifier: .pokemon5)
62 | let pokemon12 = loadPokemon(identifier: .pokemon12)
63 | let localPokemon5 = PokemonParser.parse(pokemon: pokemon5)
64 | let localPokemon12 = PokemonParser.parse(pokemon: pokemon12)
65 | appData.pokemons.append(localPokemon5)
66 | appData.pokemons.append(localPokemon12)
67 |
68 | appData.sortByOrder()
69 |
70 | guard let firstItem = appData.pokemons.first else {
71 | XCTFail("Failed to parse data")
72 | return
73 | }
74 | guard let lastItem = appData.pokemons.last else {
75 | XCTFail("Failed to parse data")
76 | return
77 | }
78 |
79 | XCTAssertTrue(firstItem.order < lastItem.order)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Features/Pokedex/Tests/GeneratorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeneratorTests.swift
3 | // PokedexTests
4 | //
5 | // Created by Ronan on 10/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Common
11 |
12 | // swiftlint:disable all
13 |
14 | @testable import Pokedex
15 |
16 | class GeneratorTests: XCTestCase {
17 | func testGenerator() {
18 |
19 | let max = 2000
20 |
21 | for _ in 0..= Constants.PokemonAPI.minIdentifier
25 | && identifier < Constants.PokemonAPI.maxIdentifier)
26 | }
27 |
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Features/Pokedex/Tests/Mocks/MockData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockData.swift
3 | // PokedexTests
4 | //
5 | // Created by Ronan on 23/11/2018.
6 | // Copyright © 2018 Sonomos. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // swiftlint:disable all
12 |
13 | class MockData {
14 | static let fileType = "json"
15 | static let fileReadError = "File not readable"
16 | static let fileNotFoundError = "File not found"
17 |
18 | static func load(name: String) throws -> Data? {
19 | let bundle = Bundle.init(for: MockData.self)
20 |
21 | if let path = bundle.path(forResource: name, ofType: fileType) {
22 | let fileUrl = URL.init(fileURLWithPath: path)
23 | do {
24 | let data = try Data.init(contentsOf: fileUrl)
25 | return data
26 | } catch {
27 | let error = fileReadError as! Error
28 | throw error
29 | }
30 | } else {
31 | let error = fileNotFoundError as! Error
32 | throw error
33 | }
34 | }
35 |
36 | static func loadNoResultsResponse() throws -> Data? {
37 | return try load(name: "noResultsServerResponse" )
38 | }
39 |
40 | static func loadResponse() throws -> Data? {
41 | return try load(name: "Pokemon5")
42 | }
43 |
44 | static func loadOtherResponse() throws -> Data? {
45 | return try load(name: "Pokemon12")
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Features/Pokedex/Tests/Mocks/Pokemon12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Pokedex/Tests/Mocks/Pokemon12.png
--------------------------------------------------------------------------------
/Features/Pokedex/Tests/Mocks/Pokemon5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Features/Pokedex/Tests/Mocks/Pokemon5.png
--------------------------------------------------------------------------------
/Features/Pokedex/Tests/PokemonParserTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PokemonParserTests.swift
3 | // PokedexTests
4 | //
5 | // Created by Ronan on 10/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Common
11 |
12 | // swiftlint:disable all
13 |
14 | @testable import Pokedex
15 |
16 | class PokemonParserTests: XCTestCase {
17 | func testParsing() {
18 | let data = try! MockData.loadResponse()
19 |
20 | let decoder = JSONDecoder()
21 | decoder.keyDecodingStrategy = .convertFromSnakeCase
22 | let pokemon = try! decoder.decode(Pokemon.self, from: data!)
23 |
24 | let localPokemon = PokemonParser.parse(pokemon: pokemon)
25 |
26 | XCTAssertNotNil(localPokemon)
27 | XCTAssertEqual(pokemon.name, localPokemon.name)
28 | XCTAssertEqual(pokemon.height, localPokemon.height)
29 | XCTAssertEqual(pokemon.weight, localPokemon.weight)
30 | XCTAssertEqual(pokemon.order, localPokemon.order)
31 | XCTAssertEqual(pokemon.baseExperience, localPokemon.baseExperience)
32 |
33 | let speciesName = pokemon.species.name
34 |
35 | XCTAssertEqual(speciesName, localPokemon.species)
36 | XCTAssertEqual(pokemon.sprites.frontDefault, localPokemon.spriteUrlString)
37 |
38 | let types = pokemon.types
39 |
40 | let typeNames = types.map {
41 | $0.type.name
42 | }
43 |
44 | XCTAssertEqual(typeNames, localPokemon.types)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Features/Pokedex/UITests/PokedexAsyncSearchUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PokedexAsyncSearchUITests.swift
3 | // PokedexUITests
4 | //
5 | // Created by ronan.ociosoig on 05/01/2022.
6 | // Copyright © 2022 Sonomos.com. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class PokedexAsyncSearchUITests: XCTestCase {
12 |
13 | let app = XCUIApplication()
14 |
15 | override func setUpWithError() throws {
16 | super.setUp()
17 | continueAfterFailure = false
18 | app.launchArguments += ["UITesting", "AsyncTesting"]
19 | app.launch()
20 |
21 | print(XCUIApplication().debugDescription)
22 | }
23 |
24 | func testSearchPokemon() {
25 | app.buttons["Ball"].tap()
26 | app.alerts["Do you want to leave it or catch it?"].buttons["Catch it!"].tap()
27 | app.buttons["Catch"].tap()
28 | app.buttons["Backpack"].tap()
29 | app.collectionViews.cells.otherElements.containing(.staticText, identifier: "Charmeleon").element.tap()
30 | app.navigationBars["Charmeleon"].buttons["Backpack"].tap()
31 |
32 | let closeButton = app.navigationBars["Backpack"].buttons["Close"]
33 | XCTAssertTrue(closeButton.waitForExistence(timeout: 1))
34 | closeButton.tap()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Features/Pokedex/UITests/PokedexUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PokedexUITests.swift
3 | // PokedexUITests
4 | //
5 | // Created by Ronan on 09/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class PokedexUITests: XCTestCase {
12 |
13 | let app = XCUIApplication()
14 |
15 | override func setUp() {
16 | super.setUp()
17 | continueAfterFailure = false
18 | app.launchArguments += ["UITesting"]
19 | app.launch()
20 |
21 | print(XCUIApplication().debugDescription)
22 | }
23 |
24 | func testSearchPokemon() {
25 | app.buttons["Ball"].tap()
26 | app.alerts["Do you want to leave it or catch it?"].buttons["Catch it!"].tap()
27 | app.buttons["Catch"].tap()
28 | app.buttons["Backpack"].tap()
29 | app.collectionViews.cells.otherElements.containing(.staticText, identifier: "Charmeleon").element.tap()
30 | app.navigationBars["Charmeleon"].buttons["Backpack"].tap()
31 |
32 | let closeButton = app.navigationBars["Backpack"].buttons["Close"]
33 | XCTAssertTrue(closeButton.waitForExistence(timeout: 1))
34 | closeButton.tap()
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Features/Pokedex/UITests/Server_401_Error_UITest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Server_401_Error_Test.swift
3 | // PokedexUITests
4 | //
5 | // Created by Ronan on 14/05/2019.
6 | // Copyright © 2019 Sonomos. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Common
11 |
12 | class Server_401_Error_Test: XCTestCase {
13 |
14 | let app = XCUIApplication()
15 |
16 | override func setUp() {
17 | super.setUp()
18 | continueAfterFailure = false
19 | app.launchArguments += ["UITesting", "Error_401"]
20 | app.launch()
21 |
22 | print(XCUIApplication().debugDescription)
23 | }
24 |
25 | func testSearchPokemon() {
26 | let app = XCUIApplication()
27 | app.buttons["Ball"].tap()
28 | app.alerts[Constants.Translations.Error.notFound].buttons["OK"].tap()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # Gemfile
2 | source "https://rubygems.org"
3 |
4 | gem "fastlane"
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 ronan.o.ciosoig
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/ModuleTargets.drawio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/ModuleTargets.drawio.png
--------------------------------------------------------------------------------
/PokedexSchemes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/PokedexSchemes.png
--------------------------------------------------------------------------------
/PokedexScreens.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/PokedexScreens.png
--------------------------------------------------------------------------------
/Project.swift:
--------------------------------------------------------------------------------
1 | import ProjectDescription
2 | import ProjectDescriptionHelpers
3 |
4 | // MARK: - Project
5 |
6 | // Creates our project using a helper function defined in ProjectDescriptionHelpers
7 | let project = Project.app(name: "Pokedex",
8 | platform: .iOS, externalDependencies: ["JGProgressHUD"],
9 | targetDependancies: [],
10 | moduleTargets: [makeHanekeModule(),
11 | makeHomeModule(),
12 | makeBackpackModule(),
13 | makeDetailModule(),
14 | makeCatchModule(),
15 | makeCommonModule(),
16 | makeNetworkModule()
17 | ])
18 | func makeHanekeModule() -> Module {
19 | return Module(name: "Haneke",
20 | path: "Haneke",
21 | frameworkDependancies: [],
22 | exampleDependencies: [],
23 | frameworkResources: [],
24 | exampleResources: ["Resources/**"],
25 | testResources: [])
26 | }
27 |
28 | func makeHomeModule() -> Module {
29 | return Module(name: "HomeUI",
30 | path: "Home",
31 | frameworkDependancies: [.target(name: "Common")],
32 | exampleDependencies: [.package(product: "JGProgressHUD")],
33 | frameworkResources: ["Sources/**/*.storyboard", "Resources/**"],
34 | exampleResources: ["Resources/**"],
35 | testResources: [])
36 | }
37 |
38 | func makeBackpackModule() -> Module {
39 | return Module(name: "BackpackUI",
40 | path: "Backpack",
41 | frameworkDependancies: [.target(name: "Common"), .target(name: "Haneke")],
42 | exampleDependencies: [.target(name: "Detail")],
43 | frameworkResources: ["Resources/**", "Sources/**/*.xib", "Sources/**/*.storyboard"],
44 | exampleResources: ["Resources/**", "Sources/**/*.storyboard"],
45 | testResources: [])
46 | }
47 |
48 | func makeDetailModule() -> Module {
49 | return Module(name: "Detail",
50 | path: "Detail",
51 | frameworkDependancies: [.target(name: "Common"), .target(name: "Haneke")],
52 | exampleDependencies: [],
53 | frameworkResources: ["Sources/**/*.storyboard"],
54 | exampleResources: ["Resources/**"],
55 | testResources: [])
56 | }
57 |
58 | func makeCatchModule() -> Module {
59 | Module(name: "CatchUI",
60 | path: "Catch",
61 | frameworkDependancies: [.target(name: "Common"), .target(name: "Haneke")],
62 | exampleDependencies: [.package(product: "JGProgressHUD"), .target(name: "NetworkKit")],
63 | frameworkResources: ["Resources/**", "Sources/**/*.storyboard"],
64 | exampleResources: ["Resources/**", "Sources/**/*.storyboard"],
65 | testResources: [])
66 | }
67 |
68 | func makeCommonModule() -> Module {
69 | return Module(name: "Common",
70 | path: "Common",
71 | frameworkDependancies: [],
72 | exampleDependencies: [],
73 | frameworkResources: ["Sources/**/*.xib"],
74 | exampleResources: ["Resources/**"],
75 | testResources: [],
76 | targets: [.framework, .unitTests])
77 | }
78 |
79 | func makeNetworkModule() -> Module {
80 | return Module(name: "NetworkKit",
81 | path: "Network",
82 | frameworkDependancies: [],
83 | exampleDependencies: [.target(name: "Common")],
84 | frameworkResources: ["Resources/**"],
85 | exampleResources: ["Resources/**"],
86 | testResources: ["**/*.json"])
87 | }
88 |
--------------------------------------------------------------------------------
/Tuist/Config.swift:
--------------------------------------------------------------------------------
1 | import ProjectDescription
2 |
3 | let config = Config(
4 | generationOptions: [
5 | // .disableAutogeneratedSchemes
6 | ]
7 | )
8 |
--------------------------------------------------------------------------------
/Tuist/Dependencies.swift:
--------------------------------------------------------------------------------
1 | import ProjectDescription
2 |
3 | let dependencies = Dependencies(
4 | carthage: [
5 | .github(path: "JonasGessner/JGProgressHUD", requirement: .upToNext("2.0.0")),
6 | ],
7 | platforms: [.iOS]
8 | )
9 |
10 |
--------------------------------------------------------------------------------
/Tuist/Templates/framework/Framework.stencil:
--------------------------------------------------------------------------------
1 | //
2 | // {{ name }}.swift
3 | // {{ name }}
4 | //
5 | // Created by {{ author }}.
6 | // Copyright © 2021. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public struct {{ name }} {
12 | public func start() {
13 |
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/Tuist/Templates/framework/UnitTests.stencil:
--------------------------------------------------------------------------------
1 | //
2 | // {{ name }}UnitTests.swift
3 | // {{ name }}Example
4 | //
5 | // Created by {{ author }}.
6 | // Copyright © 2021. All rights reserved.
7 | //
8 |
9 | @testable import {{ name }}
10 |
11 | import XCTest
12 |
13 | class {{ name }}Tests: XCTestCase {
14 | override func setUpWithError() throws {
15 | super.setUp()
16 | }
17 |
18 | override func tearDownWithError() throws {
19 | super.tearDown()
20 | }
21 |
22 | func test{{ name }}() {
23 | // Given
24 |
25 | // When
26 |
27 | // Then
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Tuist/Templates/framework/framework.swift:
--------------------------------------------------------------------------------
1 | import ProjectDescription
2 |
3 | let nameAttribute: Template.Attribute = .required("name")
4 |
5 | let template = Template(
6 | description: "Framework template",
7 | attributes: [
8 | nameAttribute,
9 | .optional("platform", default: "ios")
10 | ],
11 | files: [
12 | // Placeholder source file
13 | .file(path: "\(nameAttribute)/Sources/\(nameAttribute).swift", templatePath: "Framework.stencil"),
14 |
15 | // Placeholder UnitTest
16 | .file(path: "\(nameAttribute)/Tests/\(nameAttribute)Tests.swift", templatePath: "UnitTests.stencil")
17 | ]
18 | )
19 |
--------------------------------------------------------------------------------
/Tuist/Templates/module/ExampleAppController.stencil:
--------------------------------------------------------------------------------
1 | //
2 | // AppController.swift
3 | // {{ name }}Example
4 | //
5 | // Created by {{ author }} on {{ date }}.
6 | // Copyright © {{ year }} {{ company }}. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Common
11 |
12 | protocol AppControlling {
13 | func start()
14 | }
15 |
16 | class AppController: AppControlling {
17 | var coordinator: Coordinating?
18 |
19 | func start() {
20 | let dataProvider = DataProvider()
21 |
22 | if Configuration.uiTesting == true {
23 | let storage = FileStorage()
24 | storage.remove(AppData.pokemonFile, from: dataProvider.appData.directory())
25 | }
26 |
27 | dataProvider.start()
28 |
29 | coordinator = Coordinator()
30 | coordinator?.dataProvider = dataProvider
31 | coordinator?.start()
32 |
33 | dataProvider.notifier = coordinator as? Notifier
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Tuist/Templates/module/ExampleAppDelegate.stencil:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // {{ name }}Example
4 | //
5 | // Created by {{ author }} on {{ date }}.
6 | // Copyright © {{ year }} {{ company }}. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: NSObject, UIApplicationDelegate {
13 |
14 | private let appController = AppController()
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 |
18 | appController.start()
19 |
20 | return true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tuist/Templates/module/ExampleCoordinator.stencil:
--------------------------------------------------------------------------------
1 | //
2 | // Coordinator.swift
3 | // {{ name }}Example
4 | //
5 | // Created by {{ author }} on {{ date }}.
6 | // Copyright © {{ year }} {{ company }}. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Common
11 | import {{ name }}
12 |
13 | class Coordinator: Coordinating {
14 | let window: UIWindow
15 | var dataProvider: DataProvider?
16 |
17 | lazy var actions = Actions(coordinator: self)
18 | var presenter: Updatable?
19 | var currentViewController: UIViewController?
20 |
21 | init() {
22 | window = UIWindow(frame: UIScreen.main.bounds)
23 | window.makeKeyAndVisible()
24 | }
25 |
26 | func start() {
27 | actions.dataProvider = dataProvider
28 |
29 | showHomeScene()
30 | }
31 |
32 | func showHomeScene() {
33 | let viewController = {{ name }}ViewController()
34 | window.rootViewController = viewController
35 | }
36 |
37 | func showCatchScene() {
38 |
39 | }
40 |
41 | func searchNextPokemon() {
42 |
43 | }
44 |
45 | func showBackpackScene() {
46 |
47 | }
48 |
49 | func showPokemonDetailScene(pokemon: LocalPokemon) {
50 |
51 | }
52 |
53 | func showLoading() {
54 | showHud(with: Constants.Translations.loading)
55 | }
56 |
57 | private func showHud(with message: String) {
58 |
59 | }
60 |
61 | func dismissLoading() {
62 |
63 | }
64 |
65 | func showAlert(with message: String) {
66 | let alertController = UIAlertController(title: nil,
67 | message: message,
68 | preferredStyle: .alert)
69 |
70 | let okButton = UIAlertAction(title: Constants.Translations.ok,
71 | style: .default,
72 | handler: nil)
73 |
74 | alertController.addAction(okButton)
75 |
76 | guard let viewController = currentViewController else { return }
77 |
78 | viewController.present(alertController,
79 | animated: true,
80 | completion: nil)
81 | }
82 | }
83 |
84 | extension Coordinator: Notifier {
85 | func dataReceived(errorMessage: String?, on queue: DispatchQueue?) {
86 |
87 | var localQueue = queue
88 |
89 | if localQueue == nil {
90 | localQueue = .global(qos: .userInteractive)
91 | }
92 |
93 | localQueue?.async {
94 | self.dismissLoading()
95 |
96 | if let errorMessage = errorMessage {
97 | if errorMessage == Constants.Translations.Error.statusCode404 {
98 | self.presenter?.update()
99 | return
100 | }
101 | self.presenter?.showError(message: errorMessage)
102 | } else {
103 | self.presenter?.update()
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Tuist/Templates/module/LaunchScreen.stencil:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/Tuist/Templates/module/Module.swift:
--------------------------------------------------------------------------------
1 | import ProjectDescription
2 | import Foundation
3 |
4 | let companyName = "Sonomos"
5 | var defaultYear: String {
6 | let dateFormatter = DateFormatter()
7 | dateFormatter.dateFormat = "yyyy"
8 | return dateFormatter.string(from: Date())
9 | }
10 |
11 | var defaultDate: String {
12 | let dateFormatter = DateFormatter()
13 | dateFormatter.dateFormat = "dd/MM/yyyy"
14 | return dateFormatter.string(from: Date())
15 | }
16 |
17 | let nameAttribute: Template.Attribute = .required("name")
18 | let authorAttribute: Template.Attribute = .required("author")
19 | let yearAttribute: Template.Attribute = .optional("year", default: defaultYear)
20 | let dateAttribute: Template.Attribute = .optional("date", default: defaultDate)
21 | let companyAttribute: Template.Attribute = .optional("company", default: companyName)
22 |
23 | let template = Template(
24 | description: "Module template",
25 | attributes: [
26 | nameAttribute,
27 | authorAttribute,
28 | yearAttribute,
29 | dateAttribute,
30 | companyAttribute,
31 | .optional("platform", default: "ios")
32 | ],
33 | files: [
34 |
35 | // Placeholder source file
36 | .file(
37 | path: "\(nameAttribute)/Sources/Scenes/\(nameAttribute)ViewController.swift",
38 | templatePath: "Scene.stencil"
39 | ),
40 |
41 | // Placeholder UnitTest
42 | .file(
43 | path: "\(nameAttribute)/Tests/\(nameAttribute)Tests.swift",
44 | templatePath: "Tests.stencil"
45 | ),
46 |
47 | // Example App Icons and Launch Screen
48 | .directory(
49 | path: "\(nameAttribute)/Example",
50 | sourcePath: "Resources"
51 | ),
52 | .file(
53 | path: "\(nameAttribute)/Example/Resources/LaunchScreen.storyboard",
54 | templatePath: "LaunchScreen.stencil"
55 | ),
56 | .file(
57 | path: "\(nameAttribute)/Example/Sources/AppController.swift",
58 | templatePath: "ExampleAppController.stencil"
59 | ),
60 | .file(
61 | path: "\(nameAttribute)/Example/Sources/AppDelegate.swift",
62 | templatePath: "ExampleAppDelegate.stencil"
63 | ),
64 | .file(
65 | path: "\(nameAttribute)/Example/Sources/Coordinator.swift",
66 | templatePath: "ExampleCoordinator.stencil"
67 | )
68 | ]
69 | )
70 |
--------------------------------------------------------------------------------
/Tuist/Templates/module/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 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "PokeBallLogo_120.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "PokeBallLogo_180.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "1024x1024",
47 | "idiom" : "ios-marketing",
48 | "filename" : "PokeBallLogo_1024.png",
49 | "scale" : "1x"
50 | }
51 | ],
52 | "info" : {
53 | "version" : 1,
54 | "author" : "xcode"
55 | }
56 | }
--------------------------------------------------------------------------------
/Tuist/Templates/module/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Tuist/Templates/module/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_1024.png
--------------------------------------------------------------------------------
/Tuist/Templates/module/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Tuist/Templates/module/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_120.png
--------------------------------------------------------------------------------
/Tuist/Templates/module/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Tuist/Templates/module/Resources/Assets.xcassets/AppIcon.appiconset/PokeBallLogo_180.png
--------------------------------------------------------------------------------
/Tuist/Templates/module/Resources/Assets.xcassets/Ball.imageset/Ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/Tuist/Templates/module/Resources/Assets.xcassets/Ball.imageset/Ball.png
--------------------------------------------------------------------------------
/Tuist/Templates/module/Resources/Assets.xcassets/Ball.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Ball.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Tuist/Templates/module/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Tuist/Templates/module/Scene.stencil:
--------------------------------------------------------------------------------
1 | //
2 | // {{ name }}ViewController.swift
3 | // {{ name }}
4 | //
5 | // Created by {{ author }} on {{ date }}.
6 | // Copyright © {{ year }} {{ company }}. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public class {{ name }}ViewController: UIViewController {
12 |
13 |
14 | let label = UILabel()
15 |
16 | public override init(nibName nib: String?, bundle: Bundle?) {
17 | super.init(nibName: nib, bundle: bundle)
18 | }
19 |
20 | required init?(coder: NSCoder) {
21 | fatalError("init(coder:) has not been implemented")
22 | }
23 |
24 | public override func viewDidLoad() {
25 | title = "{{ name }}"
26 | view.backgroundColor = .white
27 |
28 | label.text = title
29 | label.textAlignment = .center
30 | label.translatesAutoresizingMaskIntoConstraints = false
31 |
32 | view.addSubview(label)
33 |
34 | NSLayoutConstraint.activate([
35 | label.leadingAnchor.constraint(equalTo: view.leadingAnchor),
36 | label.trailingAnchor.constraint(equalTo: view.trailingAnchor),
37 | label.heightAnchor.constraint(equalToConstant: 40),
38 | label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
39 | label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
40 | ])
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tuist/Templates/module/Tests.stencil:
--------------------------------------------------------------------------------
1 | //
2 | // {{ name }}Tests.swift
3 | // {{ name }}
4 | //
5 | // Created by {{ author }} on {{ date }}.
6 | // Copyright © {{ year }} {{ company }}. All rights reserved.
7 | //
8 |
9 | @testable import {{ name }}
10 |
11 | import XCTest
12 |
13 | class {{ name }}Tests: XCTestCase {
14 | override func setUpWithError() throws {
15 | super.setUp()
16 | }
17 |
18 | override func tearDownWithError() throws {
19 | super.tearDown()
20 | }
21 |
22 | func test{{ name }}() {
23 | // Given
24 |
25 | // When
26 |
27 | // Then
28 | }
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ronanociosoig/Tuist-Pokedex/7394ea502f7d8fe3b863d7c817d4a907bf7fe17d/graph.png
--------------------------------------------------------------------------------
/scripts/swiftlint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if test -d "/opt/homebrew/bin/"; then
4 | PATH="/opt/homebrew/bin/:${PATH}"
5 | fi
6 |
7 | export PATH
8 |
9 | ROOT_PATH=$1
10 | SOURCES_PATH="Features/$2/Sources"
11 |
12 | if which swiftlint >/dev/null; then
13 | swiftlint --path $ROOT_PATH/$SOURCES_PATH --config $ROOT_PATH/.swiftlint.yml --quiet
14 | else
15 | echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
16 | fi
17 |
--------------------------------------------------------------------------------