├── .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 | --------------------------------------------------------------------------------