├── .gitignore ├── LICENSE ├── NiceDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── NiceDemo.xcscheme ├── NiceDemo ├── Controllers │ ├── BaseNavigationController.swift │ └── BaseViewController.swift ├── Coordinators │ ├── AppCoordinator.swift │ ├── AuthFlowCoordinator.swift │ └── DogsFlowCoordinator.swift ├── CustomViews │ ├── HUDView.swift │ └── LoadingTableView │ │ ├── LoadingTableViewCell.swift │ │ └── LoadingTableViewProvider.swift ├── Extensions │ ├── String+Extension.swift │ ├── UIColor+Extension.swift │ ├── UIView+Extension.swift │ └── UIViewController+Extension.swift ├── Models │ ├── Dog.swift │ ├── DogBreedViewModel.swift │ ├── DogDescriptionFormatter.swift │ ├── Result.swift │ └── ServerResponses │ │ ├── BaseResponse │ │ └── BaseResponse.swift │ │ ├── GetAllDogsServerResponse.swift │ │ └── GetRandomDogImageServerResponse.swift ├── Protocols │ ├── Alertable.swift │ ├── CollectionViewProvider.swift │ ├── Coordinator.swift │ ├── HUDDisplayable.swift │ ├── ReusableView.swift │ ├── ServerService.swift │ └── TableViewProvider.swift ├── Scenes │ ├── AuthFlow │ │ ├── ForgotPassword │ │ │ ├── ForgotPasswordConfigurator.swift │ │ │ ├── ForgotPasswordContract.swift │ │ │ ├── ForgotPasswordPresenter.swift │ │ │ ├── ForgotPasswordView.swift │ │ │ └── ForgotPasswordViewController.swift │ │ └── SignIn │ │ │ ├── SignInConfigurator.swift │ │ │ ├── SignInContract.swift │ │ │ ├── SignInPresenter.swift │ │ │ ├── SignInView.swift │ │ │ └── SignInViewController.swift │ ├── DogsFlow │ │ ├── DogGallery │ │ │ ├── CollectionView │ │ │ │ ├── DogBreedCollectionViewCell.swift │ │ │ │ └── DogBreedsCollectionViewProvider.swift │ │ │ ├── DogGalleryConfigurator.swift │ │ │ ├── DogGalleryContract.swift │ │ │ ├── DogGalleryFlow.swift │ │ │ ├── DogGalleryPresenter.swift │ │ │ ├── DogGalleryView.swift │ │ │ └── DogGalleryViewController.swift │ │ └── DogsList │ │ │ ├── DogsListConfigurator.swift │ │ │ ├── DogsListContract.swift │ │ │ ├── DogsListFlow.swift │ │ │ ├── DogsListPresenter.swift │ │ │ ├── DogsListView.swift │ │ │ ├── DogsListViewController.swift │ │ │ └── TableView │ │ │ ├── DogBreedTableViewCell.swift │ │ │ └── DogsListTableViewProvider.swift │ └── Start │ │ ├── StartConfigurator.swift │ │ ├── StartContract.swift │ │ ├── StartPresenter.swift │ │ ├── StartView.swift │ │ └── StartViewController.swift ├── Services │ ├── AppDelegateService.swift │ ├── KeyboardObserver.swift │ ├── LocalStorage │ │ ├── DogsStorageService.swift │ │ └── UserCredentialsStorageService.swift │ ├── ServerApi │ │ ├── Core │ │ │ ├── ServerRouter.swift │ │ │ └── UrlSessionService.swift │ │ ├── DogsApiDataParser.swift │ │ └── DogsServerService.swift │ ├── SimpleImageLoader.swift │ ├── UserDefaultsLayer.swift │ └── Validator.swift ├── SupportingFiles │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-57x57@1x.png │ │ │ ├── Icon-App-57x57@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-72x72@1x.png │ │ │ ├── Icon-App-72x72@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ ├── Icon-Small-50x50@1x.png │ │ │ ├── Icon-Small-50x50@2x.png │ │ │ └── ItunesArtwork@2x.png │ │ ├── Contents.json │ │ ├── dog.imageset │ │ │ ├── Contents.json │ │ │ └── dog.pdf │ │ ├── pawPrint.imageset │ │ │ ├── Contents.json │ │ │ └── pawPrint.pdf │ │ ├── pawPrintNotSelected.imageset │ │ │ ├── Contents.json │ │ │ └── pawPrintNotSelected.pdf │ │ ├── pawPrintSelected.imageset │ │ │ ├── Contents.json │ │ │ └── pawPrintSelected.pdf │ │ └── walkingWithDog.imageset │ │ │ ├── Contents.json │ │ │ └── walkingWithDog.pdf │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── SystemFiles │ ├── AppDelegate.swift │ ├── TestAppDelegate.swift │ └── main.swift └── Utils │ └── Utils.swift ├── NiceDemoTests ├── AppDelegateService │ └── AppDelegateServiceTests.swift ├── Coordinators │ ├── AppCoordinatorTests.swift │ ├── AuthFlowCoordinatorTests.swift │ ├── CoordinatorTests.swift │ └── DogsFlowCoordinatorTests.swift ├── CustomViews │ ├── HUDViewTests.swift │ └── LoadingTableViewTests.swift ├── Info.plist ├── Network │ ├── ImageLoaderTests.swift │ └── NetworkTests.swift ├── Other │ └── ReusableViewTests.swift ├── Scenes │ ├── DogGallery │ │ ├── DogGalleryCollectionViewTests.swift │ │ ├── DogGalleryConfiguratorTests.swift │ │ ├── DogGalleryPresenterTests.swift │ │ └── DogGalleryViewControllerTests.swift │ ├── DogsList │ │ ├── DogsListConfiguratorTests.swift │ │ ├── DogsListSceneTests.swift │ │ └── DogsListTableViewTests.swift │ ├── ForgotPassword │ │ ├── ForgotPasswordConfiguratorTests.swift │ │ ├── ForgotPasswordPresenterTests.swift │ │ └── ForgotPasswordViewControllerTests.swift │ └── SignIn │ │ ├── SignInConfiguratorTests.swift │ │ └── SignInSceneTests.swift └── Validator │ └── ValidatorTests.swift ├── README.md └── VisualFiles ├── NiceDemo.png ├── NiceDemo.xml └── NiceDemoUI.png /.gitignore: -------------------------------------------------------------------------------- 1 | ## User settings 2 | xcuserdata/ 3 | *.xcscmblueprint 4 | *.xccheckout 5 | build/ 6 | DerivedData/ 7 | *.moved-aside 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | 17 | ### Xcode Patch ### 18 | *.xcodeproj/* 19 | !*.xcodeproj/project.pbxproj 20 | !*.xcodeproj/xcshareddata/ 21 | !*.xcworkspace/contents.xcworkspacedata 22 | /*.gcno 23 | **/xcshareddata/WorkspaceSettings.xcsettings -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Serhii Kharauzov 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 | -------------------------------------------------------------------------------- /NiceDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NiceDemo.xcodeproj/xcshareddata/xcschemes/NiceDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /NiceDemo/Controllers/BaseNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseNavigationController.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/4/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseNavigationController: UINavigationController { 12 | 13 | // MARK: Lifecycle 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | } 18 | 19 | override func didReceiveMemoryWarning() { 20 | super.didReceiveMemoryWarning() 21 | debugPrint("\(#function) at \(self)") 22 | } 23 | 24 | func makeNotTranslucent() { 25 | navigationBar.isTranslucent = false 26 | } 27 | 28 | func removeBorder() { 29 | navigationBar.setValue(true, forKey: "hidesShadow") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /NiceDemo/Controllers/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/4/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseViewController: UIViewController { 12 | 13 | // MARK: Lifecycle 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | setupNavigationBarColors() 18 | } 19 | 20 | override func didReceiveMemoryWarning() { 21 | super.didReceiveMemoryWarning() 22 | debugPrint("\(#function) at \(self)") 23 | } 24 | 25 | // MARK: Public methods 26 | 27 | func hideNavigationBar() { 28 | navigationController?.setNavigationBarHidden(true, animated: false) 29 | } 30 | 31 | func showNavigationBar() { 32 | navigationController?.setNavigationBarHidden(false, animated: false) 33 | } 34 | 35 | // MARK: Private methods 36 | 37 | private func setupNavigationBarColors() { 38 | navigationController?.navigationBar.tintColor = UIColor.AppColors.barItemTintColor 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /NiceDemo/Coordinators/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinator.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | /// Entry point among all coordinators. 13 | /// Determines, what flow must be shown based on the rules. 14 | class AppCoordinator: Coordinator { 15 | 16 | // MARK: Properties 17 | 18 | var childCoordinators: [Coordinator] = [] 19 | let navigationController: UINavigationController 20 | 21 | // MARK: Public methods 22 | 23 | init(navigationController: UINavigationController) { 24 | self.navigationController = navigationController 25 | } 26 | 27 | func start() { 28 | showStartScene() 29 | } 30 | 31 | func showStartScene() { 32 | navigationController.pushViewController(StartConfigurator().configuredViewController(delegate: self), animated: false) 33 | } 34 | 35 | func showAuthenticationFlow() { 36 | let authFlowCoordinator = AuthFlowCoordinator(navigationController: navigationController) 37 | authFlowCoordinator.delegate = self 38 | addChildCoordinator(authFlowCoordinator) 39 | authFlowCoordinator.start() 40 | } 41 | 42 | func showDogsFlow() { 43 | let dogsFlowCoordinator = DogsFlowCoordinator(navigationController: navigationController) 44 | dogsFlowCoordinator.delegate = self 45 | addChildCoordinator(dogsFlowCoordinator) 46 | dogsFlowCoordinator.start() 47 | } 48 | } 49 | 50 | // MARK: StartPresenter Delegate 51 | 52 | extension AppCoordinator: StartPresenterDelegate { 53 | func userNeedsToAuthenticate() { 54 | showAuthenticationFlow() 55 | } 56 | 57 | func userIsAuthenticated() { 58 | showDogsFlow() 59 | } 60 | } 61 | 62 | // MARK: AuthFlowCoordinator Delegate 63 | 64 | extension AppCoordinator: AuthFlowCoordinatorDelegate { 65 | func userPerformedAuthentication(coordinator: Coordinator) { 66 | removeChildCoordinator(coordinator) 67 | showDogsFlow() 68 | } 69 | } 70 | 71 | // MARK: DogsFlowCoordinator Delegate 72 | 73 | extension AppCoordinator: DogsFlowCoordinatorDelegate { 74 | 75 | } 76 | -------------------------------------------------------------------------------- /NiceDemo/Coordinators/AuthFlowCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthFlowCoordinator.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | protocol AuthFlowCoordinatorDelegate: class { 13 | func userPerformedAuthentication(coordinator: Coordinator) 14 | } 15 | 16 | /// Responsible for auth flow in the project. 17 | class AuthFlowCoordinator: Coordinator { 18 | 19 | // MARK: Properties 20 | 21 | var childCoordinators: [Coordinator] = [] 22 | let navigationController: UINavigationController 23 | weak var delegate: AuthFlowCoordinatorDelegate? 24 | 25 | // MARK: Public methods 26 | 27 | init(navigationController: UINavigationController) { 28 | self.navigationController = navigationController 29 | } 30 | 31 | func start() { 32 | showSignInScene() 33 | } 34 | 35 | func showSignInScene() { 36 | navigationController.setViewControllers([SignInConfigurator().configuredViewController(delegate: self)], animated: true) 37 | } 38 | 39 | func showForgotPasswordScene() { 40 | navigationController.pushViewController(ForgotPasswordConfigurator().configuredViewController(delegate: self), animated: true) 41 | } 42 | } 43 | 44 | // MARK: SignIn scene delegate 45 | 46 | extension AuthFlowCoordinator: SignInSceneDelegate { 47 | func handleSkipButtonTap() { 48 | userPerformedAuthentication() 49 | } 50 | 51 | func handleForgotPasswordButtonTap() { 52 | showForgotPasswordScene() 53 | } 54 | 55 | func userPerformedAuthentication() { 56 | delegate?.userPerformedAuthentication(coordinator: self) 57 | } 58 | } 59 | 60 | // MARK: ForgotPassword scene delegate 61 | 62 | extension AuthFlowCoordinator: ForgotPasswordSceneDelegate { 63 | func userPerformedPasswordRecovery() { 64 | popViewController(animated: true) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /NiceDemo/Coordinators/DogsFlowCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsFlowCoordinator.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | protocol DogsFlowCoordinatorDelegate: class { 13 | 14 | } 15 | 16 | /// Responsible for dogs flow in the project. 17 | class DogsFlowCoordinator: Coordinator { 18 | 19 | // MARK: Properties 20 | 21 | var childCoordinators: [Coordinator] = [] 22 | let navigationController: UINavigationController 23 | weak var delegate: DogsFlowCoordinatorDelegate? 24 | 25 | // MARK: Public methods 26 | 27 | init(navigationController: UINavigationController) { 28 | self.navigationController = navigationController 29 | } 30 | 31 | func start() { 32 | showDogsListScene() 33 | } 34 | 35 | func showDogsListScene() { 36 | navigationController.setViewControllers([DogsListConfigurator().configuredViewController(delegate: self)], animated: true) 37 | } 38 | 39 | func showDogGalleryScene(dog: Dog) { 40 | navigationController.pushViewController(getDogGallerySceneFrom(dog: dog), animated: true) 41 | } 42 | 43 | func getDogGallerySceneFrom(dog: Dog) -> UIViewController { 44 | return DogGalleryConfigurator().configuredViewController(dog: dog, delegate: self) 45 | } 46 | } 47 | 48 | // MARK: DogsList scene delegate 49 | 50 | extension DogsFlowCoordinator: DogsListSceneDelegate { 51 | func getGalleryView(for dog: Dog) -> UIViewController { 52 | let dogGalleryViewController = getDogGallerySceneFrom(dog: dog) 53 | // we return scene wrapped at `UINavigationController` in order to show 54 | // navigation bar at preview of Peek&Pop functional 55 | return UINavigationController(rootViewController: dogGalleryViewController) 56 | } 57 | 58 | func didSelectDog(_ dog: Dog) { 59 | showDogGalleryScene(dog: dog) 60 | } 61 | } 62 | 63 | // MARK: DogGallery scene delegate 64 | 65 | extension DogsFlowCoordinator: DogsGallerySceneDelegate { 66 | 67 | } 68 | -------------------------------------------------------------------------------- /NiceDemo/CustomViews/HUDView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingIndicatorView.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/16/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Responsible for showing dim view with loading activity indicator. 12 | 13 | class HUDView: UIView { 14 | 15 | // MARK: Private properties 16 | 17 | private var activityIndicatorView: UIActivityIndicatorView! 18 | 19 | // MARK: Init 20 | 21 | init(frame: CGRect, backgroundColor: UIColor?, tintColor: UIColor?) { 22 | super.init(frame: frame) 23 | alpha = 0 24 | translatesAutoresizingMaskIntoConstraints = false 25 | addActivityIndicatorView() 26 | self.backgroundColor = backgroundColor ?? UIColor.black.withAlphaComponent(0.2) 27 | activityIndicatorView.color = tintColor ?? .white 28 | } 29 | 30 | required init?(coder aDecoder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | // MARK: Private methods 35 | 36 | private func addActivityIndicatorView() { 37 | let activityIndicatorView = UIActivityIndicatorView(frame: .zero) 38 | activityIndicatorView.style = .whiteLarge 39 | activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false 40 | self.activityIndicatorView = activityIndicatorView 41 | addSubview(activityIndicatorView) 42 | NSLayoutConstraint(item: activityIndicatorView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1.0, constant: 0).isActive = true 43 | NSLayoutConstraint(item: activityIndicatorView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1.0, constant: 0).isActive = true 44 | NSLayoutConstraint(item: activityIndicatorView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 0).isActive = true 45 | NSLayoutConstraint(item: activityIndicatorView, attribute: .width, relatedBy: .equal, toItem: activityIndicatorView, attribute: .height, multiplier: 1.0, constant: 0).isActive = true 46 | } 47 | } 48 | 49 | // MARK: Public methods 50 | 51 | extension HUDView { 52 | func startAnimating() { 53 | activityIndicatorView.startAnimating() 54 | } 55 | 56 | func stopAnimating() { 57 | activityIndicatorView.stopAnimating() 58 | } 59 | 60 | func isAnimating() -> Bool { 61 | return activityIndicatorView.isAnimating 62 | } 63 | 64 | /// Returns `HUDView` if it is subview of view. Else returns nil. 65 | static func hudIn(view: UIView) -> HUDView? { 66 | return view.subviews.filter({$0 is HUDView}).first as? HUDView 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /NiceDemo/CustomViews/LoadingTableView/LoadingTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingTableViewCell.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 1/16/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LoadingTableViewCell: UITableViewCell { 12 | 13 | let appearance = Appearance() 14 | lazy var avatarView = getConfiguredEmptyView(radius: appearance.avatarViewSize.width / 2) 15 | lazy var titleView = getConfiguredEmptyView(radius: appearance.titleCornerRadius) 16 | lazy var subtitleView = getConfiguredEmptyView(radius: appearance.titleCornerRadius) 17 | 18 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 19 | super.init(style: style, reuseIdentifier: reuseIdentifier) 20 | initialise() 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | private func initialise() { 28 | selectionStyle = .none 29 | contentView.addSubview(avatarView) 30 | contentView.addSubview(titleView) 31 | contentView.addSubview(subtitleView) 32 | setupConstraints() 33 | } 34 | 35 | private func setupConstraints() { 36 | // setting constraints for avatarView 37 | NSLayoutConstraint(item: avatarView, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1.0, constant: 10).isActive = true 38 | NSLayoutConstraint(item: avatarView, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1.0, constant: 10).isActive = true 39 | NSLayoutConstraint(item: avatarView, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1.0, constant: -10).isActive = true 40 | NSLayoutConstraint(item: avatarView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: appearance.avatarViewSize.height).isActive = true 41 | NSLayoutConstraint(item: avatarView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: appearance.avatarViewSize.width).isActive = true 42 | // setting constraints for titleView 43 | NSLayoutConstraint(item: titleView, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1.0, constant: 15).isActive = true 44 | NSLayoutConstraint(item: titleView, attribute: .leading, relatedBy: .equal, toItem: avatarView, attribute: .trailing, multiplier: 1.0, constant: 12).isActive = true 45 | NSLayoutConstraint(item: titleView, attribute: .width, relatedBy: .equal, toItem: contentView, attribute: .width, multiplier: 0.2, constant: 0).isActive = true 46 | NSLayoutConstraint(item: titleView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: appearance.lineViewHeight).isActive = true 47 | // setting constraints for subtitleView 48 | NSLayoutConstraint(item: subtitleView, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1.0, constant: -15).isActive = true 49 | NSLayoutConstraint(item: subtitleView, attribute: .leading, relatedBy: .equal, toItem: avatarView, attribute: .trailing, multiplier: 1.0, constant: 12).isActive = true 50 | NSLayoutConstraint(item: subtitleView, attribute: .width, relatedBy: .equal, toItem: contentView, attribute: .width, multiplier: 0.35, constant: 0).isActive = true 51 | NSLayoutConstraint(item: subtitleView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: appearance.lineViewHeight).isActive = true 52 | } 53 | 54 | private func getConfiguredEmptyView(radius: CGFloat) -> UIView { 55 | let view = UIView() 56 | view.translatesAutoresizingMaskIntoConstraints = false 57 | view.backgroundColor = UIColor.AppColors.primaryColor.withAlphaComponent(0.3) 58 | view.layer.cornerRadius = radius 59 | return view 60 | } 61 | } 62 | 63 | extension LoadingTableViewCell { 64 | struct Appearance { 65 | let avatarViewSize = CGSize(width: 50, height: 50) 66 | let lineViewHeight: CGFloat = 8 67 | let titleCornerRadius: CGFloat = 4 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /NiceDemo/CustomViews/LoadingTableView/LoadingTableViewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingTableViewProvider.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 1/16/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class LoadingTableViewProvider: NSObject, TableViewProvider { 13 | 14 | let configuration: Configuration 15 | 16 | init(configuration: Configuration = Configuration()) { 17 | self.configuration = configuration 18 | } 19 | 20 | func numberOfSections(in tableView: UITableView) -> Int { 21 | return configuration.numberOfSections 22 | } 23 | 24 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 25 | return configuration.numberOfRowsInSection 26 | } 27 | 28 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 29 | return tableView.dequeueReusableCell(withIdentifier: LoadingTableViewCell.reuseIdentifier, for: indexPath) as? LoadingTableViewCell ?? UITableViewCell() 30 | } 31 | 32 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 33 | return configuration.heightForRow 34 | } 35 | } 36 | 37 | extension LoadingTableViewProvider { 38 | struct Configuration { 39 | let numberOfSections: Int 40 | let numberOfRowsInSection: Int 41 | let heightForRow: CGFloat 42 | 43 | init(numberOfSections: Int = 1, numberOfRowsInSection: Int = 100, heightForRow: CGFloat = 70) { 44 | self.numberOfSections = numberOfSections 45 | self.numberOfRowsInSection = numberOfRowsInSection 46 | self.heightForRow = heightForRow 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /NiceDemo/Extensions/String+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extension.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 12/29/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | func capitalizingFirstLetter() -> String { 13 | return prefix(1).uppercased() + dropFirst() 14 | } 15 | 16 | mutating func capitalizeFirstLetter() { 17 | self = self.capitalizingFirstLetter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /NiceDemo/Extensions/UIColor+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Extension.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 12/27/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIColor { 13 | struct AppColors { 14 | static let primaryColor = #colorLiteral(red: 0.2431372549, green: 0.3607843137, blue: 0.5019607843, alpha: 1) 15 | static let secondaryColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) 16 | static let barItemTintColor = primaryColor 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NiceDemo/Extensions/UIView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extensions.swift 3 | // 4 | // 5 | // Created by Serhii Kharauzov on 1/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIView: ReusableView, HUDDisplayable {} 13 | 14 | extension UIView { 15 | func addSketchShadow(color: UIColor, alpha: Float, x: CGFloat, y: CGFloat, blur: CGFloat, spread: CGFloat) { 16 | layer.shadowColor = color.cgColor 17 | layer.shadowOpacity = alpha 18 | layer.shadowOffset = CGSize(width: x, height: y) 19 | layer.shadowRadius = blur / 2.0 20 | if spread == 0 { 21 | layer.shadowPath = nil 22 | } else { 23 | let dx = -spread 24 | let rect = bounds.insetBy(dx: dx, dy: dx) 25 | layer.shadowPath = UIBezierPath(rect: rect).cgPath 26 | } 27 | } 28 | 29 | func hideSketchShadow() { 30 | layer.shadowOpacity = 0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /NiceDemo/Extensions/UIViewController+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Extension.swift 3 | // 4 | // 5 | // Created by Serhii Kharauzov on 1/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIViewController: ReusableView, Alertable {} 13 | 14 | extension UIViewController { 15 | /// Hides keyboard of active textField of `UIViewController`. 16 | func dismissKeyboard(force: Bool = true) { 17 | view.endEditing(true) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /NiceDemo/Models/Dog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dog.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Dog { 12 | let breed: String 13 | let subbreeds: [String]? 14 | } 15 | -------------------------------------------------------------------------------- /NiceDemo/Models/DogBreedViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogBreedViewModel.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 17.04.2020. 6 | // Copyright © 2020 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct DogBreedViewModel { 12 | let title: String 13 | let subtitle: String 14 | } 15 | -------------------------------------------------------------------------------- /NiceDemo/Models/DogDescriptionFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogDescriptionFormatter.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 12/27/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct DogDescriptionFormatter { 12 | func getBreedDescriptionFrom(dog: Dog) -> (title: String, subtitle: String) { 13 | let dogBreedTitle = dog.breed.capitalizingFirstLetter() 14 | let dogSubreedSubtitle: String 15 | if let dogSubreeds = dog.subbreeds, !dogSubreeds.isEmpty { 16 | dogSubreedSubtitle = "Has subreeds: (\(dogSubreeds.count))" 17 | } else { 18 | dogSubreedSubtitle = "No subreeds" 19 | } 20 | return (dogBreedTitle, dogSubreedSubtitle) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NiceDemo/Models/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/14/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The result of response serialization. 12 | enum Result { 13 | case success(T) 14 | case failure(Error) 15 | } 16 | -------------------------------------------------------------------------------- /NiceDemo/Models/ServerResponses/BaseResponse/BaseResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseResponse.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/13/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class BaseResponse: Codable { 12 | let status: String 13 | let error: String? 14 | } 15 | -------------------------------------------------------------------------------- /NiceDemo/Models/ServerResponses/GetAllDogsServerResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAllDogsServerResponse.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/13/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class GetAllDogsServerResponse: BaseResponse { 12 | var formattedData: [Dog]? { 13 | guard let data = data else { return nil } 14 | var dogs = [Dog]() 15 | data.forEach({ (item) in 16 | dogs.append(Dog(breed: item.key, subbreeds: item.value)) 17 | }) 18 | return dogs.sorted(by: {$0.breed < $1.breed}) 19 | } 20 | 21 | private let data: [String: [String]]? 22 | 23 | required init(from decoder: Decoder) throws { 24 | let values = try decoder.container(keyedBy: CodingKeys.self) 25 | self.data = try values.decode([String: [String]].self, forKey: .data) 26 | try super.init(from: decoder) 27 | } 28 | 29 | override func encode(to encoder: Encoder) throws { 30 | } 31 | } 32 | 33 | extension GetAllDogsServerResponse { 34 | enum CodingKeys: String, CodingKey { 35 | case data = "message" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /NiceDemo/Models/ServerResponses/GetRandomDogImageServerResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetRandomDogImageServerResponse.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/15/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class GetRandomDogImageServerResponse: BaseResponse { 12 | var data: String? 13 | 14 | required init(from decoder: Decoder) throws { 15 | let values = try decoder.container(keyedBy: CodingKeys.self) 16 | self.data = try values.decode(String.self, forKey: .data) 17 | try super.init(from: decoder) 18 | } 19 | 20 | override func encode(to encoder: Encoder) throws { 21 | } 22 | } 23 | 24 | extension GetRandomDogImageServerResponse { 25 | enum CodingKeys: String, CodingKey { 26 | case data = "message" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NiceDemo/Protocols/Alertable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Alertable.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/19/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | /// Defines protocol for presenting `UIAlertController`. 13 | protocol Alertable { 14 | /// Defines method for presenting `UIAlertController`. 15 | func presentAlert(title: String?, message: String?, defaultButtonTitle: String?, completion: (() -> Void)?) 16 | } 17 | 18 | extension Alertable where Self: UIViewController { 19 | func presentAlert(title: String?, message: String?, defaultButtonTitle: String? = "Okay", completion: (() -> Void)? = nil) { 20 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) 21 | alertController.addAction(UIAlertAction(title: defaultButtonTitle, style: .default, handler: { (action) in 22 | completion?() 23 | })) 24 | present(alertController, animated: true, completion: nil) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NiceDemo/Protocols/CollectionViewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewProvider.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 12/27/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | protocol CollectionViewProvider: UICollectionViewDataSource, UICollectionViewDelegate { } 13 | -------------------------------------------------------------------------------- /NiceDemo/Protocols/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // 4 | // 5 | // Created by Serhii Kharauzov on 1/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | protocol Coordinator: class { 13 | var childCoordinators: [Coordinator] { get set } 14 | var navigationController: UINavigationController { get } 15 | 16 | func start() 17 | func addChildCoordinator(_ coordinator: Coordinator) 18 | func removeChildCoordinator(_ coordinator: Coordinator) 19 | func popViewController(animated: Bool) 20 | func dismissViewController(animated: Bool, completion: (() -> Void)?) 21 | } 22 | 23 | extension Coordinator { 24 | func addChildCoordinator(_ coordinator: Coordinator) { 25 | for element in childCoordinators { 26 | if element === coordinator { 27 | return 28 | } 29 | } 30 | childCoordinators.append(coordinator) 31 | } 32 | 33 | func removeChildCoordinator(_ coordinator: Coordinator) { 34 | guard !childCoordinators.isEmpty else { return } 35 | for (index, element) in childCoordinators.enumerated() { 36 | if element === coordinator { 37 | childCoordinators.remove(at: index) 38 | break 39 | } 40 | } 41 | } 42 | 43 | func popViewController(animated: Bool) { 44 | navigationController.popViewController(animated: animated) 45 | } 46 | 47 | func dismissViewController(animated: Bool, completion: (() -> Void)?) { 48 | navigationController.dismiss(animated: animated, completion: completion) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NiceDemo/Protocols/HUDDisplayable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HUDDisplayable.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/19/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | /// Displays animating HUD above `UIView`. 13 | protocol HUDDisplayable { 14 | func showHUD(animated: Bool) 15 | func showHUD(backgroundColor: UIColor?, tintColor: UIColor?, animated: Bool) 16 | func hideHUD(animated: Bool) 17 | } 18 | 19 | extension HUDDisplayable where Self: UIView { 20 | func showHUD(animated: Bool) { 21 | showHUD(backgroundColor: nil, tintColor: nil, animated: animated) 22 | } 23 | 24 | func showHUD(backgroundColor: UIColor?, tintColor: UIColor?, animated: Bool) { 25 | // check, if view has presented `HUD` already. 26 | // to avoid dublication of `HUD` 27 | guard HUDView.hudIn(view: self) == nil else { return } 28 | let hudView = HUDView(frame: .zero, backgroundColor: backgroundColor, tintColor: tintColor) 29 | addSubview(hudView) 30 | pinEdgesOf(hudView, to: self) 31 | hudView.startAnimating() 32 | if animated { 33 | UIView.animate(withDuration: 1, animations: { 34 | hudView.alpha = 1 35 | }) 36 | } else { 37 | hudView.alpha = 1 38 | } 39 | } 40 | 41 | func hideHUD(animated: Bool) { 42 | // check, if view has presented `HUD` already. 43 | // to have `HUD` for hiding.. 44 | guard let hud = HUDView.hudIn(view: self) else { return } 45 | hud.stopAnimating() 46 | if animated { 47 | UIView.animate(withDuration: 1, animations: { 48 | hud.alpha = 0 49 | }, completion: { (completed) in 50 | hud.removeFromSuperview() 51 | }) 52 | } else { 53 | hud.removeFromSuperview() 54 | } 55 | } 56 | 57 | private func pinEdgesOf(_ view: UIView, to superView: UIView) { 58 | NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: superView, attribute: .top, multiplier: 1.0, constant: 0).isActive = true 59 | NSLayoutConstraint(item: view, attribute: .bottom, relatedBy: .equal, toItem: superView, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true 60 | NSLayoutConstraint(item: view, attribute: .leading, relatedBy: .equal, toItem: superView, attribute: .leading, multiplier: 1.0, constant: 0).isActive = true 61 | NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, toItem: superView, attribute: .trailing, multiplier: 1.0, constant: 0).isActive = true 62 | } 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /NiceDemo/Protocols/ReusableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReusableView.swift 3 | // 4 | // 5 | // Created by Serhii Kharauzov on 1/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Object, that adopts this protocol, will use identifier that matches name of its class. 12 | protocol ReusableView: class {} 13 | 14 | extension ReusableView { 15 | static var reuseIdentifier: String { 16 | return String(describing: self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NiceDemo/Protocols/ServerService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerService.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/15/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias ServerRequestResponseCompletion = (Result) -> Void 12 | 13 | protocol ServerService { 14 | func performRequest(_ request: URLRequestable, completion: ServerRequestResponseCompletion?) 15 | } 16 | -------------------------------------------------------------------------------- /NiceDemo/Protocols/TableViewProvider.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // TableViewProvider.swift 4 | // NiceDemo 5 | // 6 | // Created by Serhii Kharauzov on 12/27/18. 7 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 8 | // 9 | 10 | import Foundation 11 | import UIKit 12 | 13 | protocol TableViewProvider: UITableViewDataSource, UITableViewDelegate { } 14 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/AuthFlow/ForgotPassword/ForgotPasswordConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordConfigurator.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ForgotPasswordConfigurator { 12 | 13 | /// Returns viewController, configured with its associated presenter. 14 | func configuredViewController(delegate: ForgotPasswordSceneDelegate?) -> ForgotPasswordViewController { 15 | let viewController = ForgotPasswordViewController() 16 | let presenter = ForgotPasswordPresenter(view: viewController) 17 | presenter.delegate = delegate 18 | viewController.setPresenter(presenter) 19 | return viewController 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/AuthFlow/ForgotPassword/ForgotPasswordContract.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordContract.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ForgotPasswordViewInterface: class { 12 | func hideKeyboard() 13 | func getEmailText() -> String? 14 | func showAlert(title: String?, message: String?, completion: (() -> Void)?) 15 | } 16 | 17 | protocol ForgotPasswordPresentation: class { 18 | func handleSubmitButtonTap() 19 | } 20 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/AuthFlow/ForgotPassword/ForgotPasswordPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordPresenter.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ForgotPasswordSceneDelegate: class { 12 | func userPerformedPasswordRecovery() 13 | } 14 | 15 | class ForgotPasswordPresenter { 16 | 17 | // MARK: Public properties 18 | 19 | weak var delegate: ForgotPasswordSceneDelegate? 20 | weak var view: ForgotPasswordViewInterface! 21 | let validator = Validator() 22 | 23 | // MARK: Public methods 24 | 25 | init(view: ForgotPasswordViewInterface) { 26 | self.view = view 27 | } 28 | 29 | /// Returns value if it is valid. In other case returns nil. 30 | func validateEmail(_ value: String?) -> String? { 31 | do { 32 | try validator.validateEmail(value) 33 | } catch let error as Validator.ValidationError { 34 | view.showAlert(title: nil, message: error.localizedDescription, completion: nil) 35 | return nil 36 | } catch { 37 | return nil 38 | } 39 | return value 40 | } 41 | 42 | func performPasswordRecovery(using email: String) { 43 | // perform some url request 44 | // handle result 45 | if true { 46 | view.showAlert(title: nil, message: "We sent you instructions for password recovery on your email.", completion: { [unowned self] in 47 | self.delegate?.userPerformedPasswordRecovery() 48 | }) 49 | } else { 50 | // show appropriate UI, that error occured 51 | } 52 | } 53 | } 54 | 55 | // MARK: ForgotPasswordPresentation Protocol 56 | 57 | extension ForgotPasswordPresenter: ForgotPasswordPresentation { 58 | func handleSubmitButtonTap() { 59 | view.hideKeyboard() 60 | let email = view.getEmailText() 61 | // perform some validation here 62 | if let validatededEmail = validateEmail(email) { 63 | // validation passed 64 | // show loading... 65 | performPasswordRecovery(using: validatededEmail) 66 | // hide loading.. 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/AuthFlow/ForgotPassword/ForgotPasswordView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordView.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ForgotPasswordView: UIView { 12 | 13 | // MARK: Public properties 14 | 15 | var didPressSubmitButton: (() -> Void)? 16 | 17 | // MARK: Private properties 18 | 19 | let submitButton: UIButton = { 20 | let button = UIButton(type: .roundedRect) 21 | button.setTitle("Submit", for: .normal) 22 | button.setTitleColor(UIColor.AppColors.secondaryColor, for: .normal) 23 | button.layer.cornerRadius = 10.0 24 | button.backgroundColor = UIColor.AppColors.primaryColor 25 | button.titleLabel?.font = UIFont.systemFont(ofSize: 22, weight: .semibold) 26 | button.addTarget(self, action: #selector(submitButtonTapped(_:)), for: .touchUpInside) 27 | button.translatesAutoresizingMaskIntoConstraints = false 28 | return button 29 | }() 30 | let emailTextField: UITextField = { 31 | let textField = UITextField(frame: .zero) 32 | textField.placeholder = "Email" 33 | textField.keyboardType = .emailAddress 34 | textField.autocapitalizationType = .none 35 | textField.autocorrectionType = .no 36 | textField.translatesAutoresizingMaskIntoConstraints = false 37 | return textField 38 | }() 39 | let descriptionLabel: UILabel = { 40 | let label = UILabel(frame: .zero) 41 | label.font = UIFont.systemFont(ofSize: 14, weight: .regular) 42 | label.textColor = UIColor.AppColors.primaryColor 43 | label.textAlignment = .left 44 | label.text = "Enter your email and we’ll send you a link to get back to your account." 45 | label.numberOfLines = 0 46 | label.translatesAutoresizingMaskIntoConstraints = false 47 | return label 48 | }() 49 | let logoImageView: UIImageView = { 50 | let imageView = UIImageView(image: #imageLiteral(resourceName: "walkingWithDog").withRenderingMode(.alwaysTemplate)) 51 | imageView.tintColor = UIColor.AppColors.primaryColor 52 | imageView.contentMode = .scaleAspectFill 53 | imageView.clipsToBounds = true 54 | imageView.translatesAutoresizingMaskIntoConstraints = false 55 | return imageView 56 | }() 57 | 58 | // MARK: Init 59 | 60 | override init(frame: CGRect) { 61 | super.init(frame: frame) 62 | backgroundColor = .white 63 | emailTextField.delegate = self 64 | addLogoImageView() 65 | addEmailTextField() 66 | addDescriptionLabel() 67 | addForgotPasswordButton() 68 | } 69 | 70 | required init?(coder aDecoder: NSCoder) { 71 | fatalError("init(coder:) has not been implemented") 72 | } 73 | 74 | // MARK: Private methods 75 | 76 | private func addLogoImageView() { 77 | addSubview(logoImageView) 78 | NSLayoutConstraint(item: logoImageView, attribute: .top, relatedBy: .equal, toItem: safeAreaLayoutGuide, attribute: .top, multiplier: 1.0, constant: 44).isActive = true 79 | NSLayoutConstraint(item: logoImageView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1.0, constant: 0).isActive = true 80 | NSLayoutConstraint(item: logoImageView, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 0.25, constant: 0).isActive = true 81 | NSLayoutConstraint(item: logoImageView, attribute: .height, relatedBy: .equal, toItem: logoImageView, attribute: .width, multiplier: 1.0, constant: 0).isActive = true 82 | } 83 | 84 | private func addEmailTextField() { 85 | addSubview(emailTextField) 86 | NSLayoutConstraint(item: emailTextField, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1.0, constant: 0).isActive = true 87 | NSLayoutConstraint(item: emailTextField, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: 16.0).isActive = true 88 | NSLayoutConstraint(item: emailTextField, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: -16.0).isActive = true 89 | NSLayoutConstraint(item: emailTextField, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 44.0).isActive = true 90 | 91 | let lineView = UIView() 92 | lineView.backgroundColor = .lightGray 93 | lineView.translatesAutoresizingMaskIntoConstraints = false 94 | addSubview(lineView) 95 | NSLayoutConstraint(item: lineView, attribute: .top, relatedBy: .equal, toItem: emailTextField, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true 96 | NSLayoutConstraint(item: lineView, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: 16.0).isActive = true 97 | NSLayoutConstraint(item: lineView, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: -16.0).isActive = true 98 | NSLayoutConstraint(item: lineView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 0.5).isActive = true 99 | } 100 | 101 | private func addDescriptionLabel() { 102 | addSubview(descriptionLabel) 103 | NSLayoutConstraint(item: descriptionLabel, attribute: .bottom, relatedBy: .equal, toItem: emailTextField, attribute: .top, multiplier: 1.0, constant: -24).isActive = true 104 | NSLayoutConstraint(item: descriptionLabel, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: 16.0).isActive = true 105 | NSLayoutConstraint(item: descriptionLabel, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: -16.0).isActive = true 106 | } 107 | 108 | private func addForgotPasswordButton() { 109 | addSubview(submitButton) 110 | if DeviceType.hasTopNotch { 111 | NSLayoutConstraint(item: submitButton, attribute: .bottom, relatedBy: .equal, toItem: safeAreaLayoutGuide, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true 112 | } else { 113 | NSLayoutConstraint(item: submitButton, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -16).isActive = true 114 | } 115 | NSLayoutConstraint(item: submitButton, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: 16.0).isActive = true 116 | NSLayoutConstraint(item: submitButton, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: -16.0).isActive = true 117 | NSLayoutConstraint(item: submitButton, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 54.0).isActive = true 118 | } 119 | 120 | @objc private func submitButtonTapped(_ sender: UIButton) { 121 | didPressSubmitButton?() 122 | } 123 | } 124 | 125 | // MARK: UITextField Delegate 126 | 127 | extension ForgotPasswordView: UITextFieldDelegate { 128 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 129 | return textField.resignFirstResponder() 130 | } 131 | } 132 | 133 | // MARK: ForgotPasswordView public methods 134 | 135 | extension ForgotPasswordView { 136 | func getEmailText() -> String? { 137 | return emailTextField.text 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/AuthFlow/ForgotPassword/ForgotPasswordViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordViewController.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class ForgotPasswordViewController: BaseViewController { 13 | 14 | // MARK: Properties 15 | 16 | var presenter: ForgotPasswordPresentation! 17 | lazy var customView = view as! ForgotPasswordView 18 | 19 | // MARK: Lifecycle 20 | 21 | override func loadView() { 22 | view = ForgotPasswordView(frame: UIScreen.main.bounds) 23 | } 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | navigationItem.title = "Recover password" 28 | subscribeOnCustomViewActions() 29 | } 30 | 31 | // MARK: Public methods 32 | 33 | func setPresenter(_ presenter: ForgotPasswordPresentation) { 34 | self.presenter = presenter 35 | } 36 | 37 | func subscribeOnCustomViewActions() { 38 | customView.didPressSubmitButton = { [unowned self] in 39 | self.presenter.handleSubmitButtonTap() 40 | } 41 | } 42 | } 43 | 44 | // MARK: ForgotPasswordViewInterface 45 | 46 | extension ForgotPasswordViewController: ForgotPasswordViewInterface { 47 | func hideKeyboard() { 48 | dismissKeyboard() 49 | } 50 | 51 | func getEmailText() -> String? { 52 | return customView.getEmailText() 53 | } 54 | 55 | func showAlert(title: String?, message: String?, completion: (() -> Void)?) { 56 | presentAlert(title: title, message: message, defaultButtonTitle: "Okay") { 57 | completion?() 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/AuthFlow/SignIn/SignInConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInConfigurator.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SignInConfigurator { 12 | 13 | /// Returns viewController, configured with its associated presenter. 14 | func configuredViewController(delegate: SignInSceneDelegate?) -> SignInViewController { 15 | let viewController = SignInViewController() 16 | let presenter = SignInPresenter(view: viewController, userCredentialsStorage: UserCredentialsStorageService()) 17 | presenter.delegate = delegate 18 | viewController.setPresenter(presenter) 19 | return viewController 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/AuthFlow/SignIn/SignInContract.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInContract.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol SignInViewInterface: class { 12 | func hideKeyboard() 13 | func getPasswordString() -> String? 14 | func getEmailString() -> String? 15 | func showAlert(title: String?, message: String?) 16 | } 17 | 18 | protocol SignInPresentation: class { 19 | func handleSignInButtonTap() 20 | func handleSkipButtonTap() 21 | func handleForgotPasswordButtonTap() 22 | } 23 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/AuthFlow/SignIn/SignInPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInPresenter.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol SignInSceneDelegate: class { 12 | func handleSkipButtonTap() 13 | func handleForgotPasswordButtonTap() 14 | func userPerformedAuthentication() 15 | } 16 | 17 | class SignInPresenter { 18 | 19 | // MARK: Public properties 20 | 21 | weak var delegate: SignInSceneDelegate? 22 | weak var view: SignInViewInterface! 23 | let userCredentialsStorage: UserCredentialsStorageService 24 | let validator = Validator() 25 | 26 | // MARK: Public methods 27 | 28 | init(view: SignInViewInterface, userCredentialsStorage: UserCredentialsStorageService) { 29 | self.view = view 30 | self.userCredentialsStorage = userCredentialsStorage 31 | } 32 | 33 | /// Returns value if it is valid. In other case returns nil. 34 | func validateEmail(_ value: String?) -> String? { 35 | do { 36 | try validator.validateEmail(value) 37 | } catch let error as Validator.ValidationError { 38 | view.showAlert(title: nil, message: error.localizedDescription) 39 | return nil 40 | } catch { 41 | return nil 42 | } 43 | return value 44 | } 45 | 46 | /// Returns value if it is valid. In other case returns nil. 47 | func validatePassword(_ value: String?) -> String? { 48 | do { 49 | try validator.validatePassword(value) 50 | } catch let error as Validator.ValidationError { 51 | view.showAlert(title: nil, message: error.localizedDescription) 52 | return nil 53 | } catch { 54 | return nil 55 | } 56 | return value 57 | } 58 | } 59 | 60 | // MARK: SignInPresentation Protocol 61 | 62 | extension SignInPresenter: SignInPresentation { 63 | func handleSignInButtonTap() { 64 | let email = view.getEmailString() 65 | let password = view.getPasswordString() 66 | // perform some validation here 67 | if let _ = validateEmail(email), let _ = validatePassword(password) { 68 | // validation passed 69 | // show loading... 70 | // perform some url request 71 | // hide loading.. 72 | // handle result 73 | if true { 74 | // we store, that user authenticated successfully 75 | userCredentialsStorage.isUserAuthenticated = true 76 | delegate?.userPerformedAuthentication() 77 | } else { 78 | // show appropriate UI, that error occured 79 | } 80 | } 81 | } 82 | 83 | func handleSkipButtonTap() { 84 | delegate?.handleSkipButtonTap() 85 | } 86 | 87 | func handleForgotPasswordButtonTap() { 88 | delegate?.handleForgotPasswordButtonTap() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/AuthFlow/SignIn/SignInViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInViewController.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class SignInViewController: BaseViewController { 13 | 14 | // MARK: Properties 15 | 16 | var presenter: SignInPresentation! 17 | lazy var customView = view as! SignInView 18 | 19 | // MARK: Lifecycle 20 | 21 | override func loadView() { 22 | view = SignInView(frame: UIScreen.main.bounds) 23 | } 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | setupNavigationItem() 28 | showNavigationBar() 29 | subscribeOnCustomViewActions() 30 | } 31 | 32 | // MARK: Public methods 33 | 34 | func setPresenter(_ presenter: SignInPresentation) { 35 | self.presenter = presenter 36 | } 37 | 38 | func setupNavigationItem() { 39 | navigationItem.title = "Sign In" 40 | let skipBarButtonItem = UIBarButtonItem(title: "Skip", style: .done, target: self, action: #selector(skipButtonTapped)) 41 | navigationItem.rightBarButtonItem = skipBarButtonItem 42 | } 43 | 44 | @objc func skipButtonTapped() { 45 | presenter.handleSkipButtonTap() 46 | } 47 | 48 | func subscribeOnCustomViewActions() { 49 | customView.didPressForgotPasswordButton = { [unowned self] in 50 | self.presenter.handleForgotPasswordButtonTap() 51 | } 52 | customView.didPressSignInButton = { [unowned self] in 53 | self.presenter.handleSignInButtonTap() 54 | } 55 | } 56 | } 57 | 58 | // MARK: SignInViewInterface 59 | 60 | extension SignInViewController: SignInViewInterface { 61 | func hideKeyboard() { 62 | dismissKeyboard() 63 | } 64 | 65 | func getPasswordString() -> String? { 66 | return customView.getPasswordText() 67 | } 68 | 69 | func getEmailString() -> String? { 70 | return customView.getEmailText() 71 | } 72 | 73 | func setEmailText(_ value: String) { 74 | customView.setEmailText(value) 75 | } 76 | 77 | func setPasswordText(_ value: String) { 78 | customView.setPasswordText(value) 79 | } 80 | 81 | func showAlert(title: String?, message: String?) { 82 | presentAlert(title: title, message: message) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogGallery/CollectionView/DogBreedCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogBreedCollectionViewCell.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 1/5/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DogBreedCollectionViewCell: UICollectionViewCell { 12 | 13 | // MARK: Private properties 14 | 15 | let titleLabel: UILabel = { 16 | let label = UILabel() 17 | label.translatesAutoresizingMaskIntoConstraints = false 18 | label.font = UIFont.systemFont(ofSize: 15, weight: .semibold) 19 | label.textColor = .black 20 | label.textAlignment = .center 21 | return label 22 | }() 23 | 24 | // MARK: Init 25 | 26 | override init(frame: CGRect) { 27 | super.init(frame: frame) 28 | initialise() 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | // MARK: Public methods 36 | 37 | func setTitle(_ title: String) { 38 | titleLabel.text = title 39 | } 40 | 41 | // MARK: Private methods 42 | 43 | private func initialise() { 44 | contentView.addSubview(titleLabel) 45 | setupBorder() 46 | setupConstraints() 47 | } 48 | 49 | private func setupBorder() { 50 | layer.borderColor = UIColor.black.cgColor 51 | layer.borderWidth = 2 52 | layer.cornerRadius = 10 53 | } 54 | 55 | private func setupConstraints() { 56 | NSLayoutConstraint(item: titleLabel, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1.0, constant: 0).isActive = true 57 | NSLayoutConstraint(item: titleLabel, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true 58 | NSLayoutConstraint(item: titleLabel, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1.0, constant: 20).isActive = true 59 | NSLayoutConstraint(item: titleLabel, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1.0, constant: -20).isActive = true 60 | NSLayoutConstraint(item: titleLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: DogBreedCollectionViewCell.height).isActive = true 61 | } 62 | } 63 | 64 | extension DogBreedCollectionViewCell { 65 | static let height: CGFloat = 55.0 66 | } 67 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogGallery/CollectionView/DogBreedsCollectionViewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogBreedsCollectionViewProvider.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 1/5/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class DogBreedsCollectionViewProvider: NSObject, CollectionViewProvider { 13 | 14 | var data = [String]() 15 | 16 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 17 | return data.count 18 | } 19 | 20 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 21 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DogBreedCollectionViewCell.reuseIdentifier, for: indexPath) as? DogBreedCollectionViewCell else { 22 | return UICollectionViewCell() 23 | } 24 | cell.setTitle(data[indexPath.row]) 25 | return cell 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogGallery/DogGalleryConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogGalleryConfigurator.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/15/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class DogGalleryConfigurator { 12 | 13 | /// Returns viewController, configured with its associated presenter. 14 | func configuredViewController(dog: Dog, delegate: DogsGallerySceneDelegate?) -> DogGalleryViewController { 15 | let viewController = DogGalleryViewController() 16 | let presenter = DogGalleryPresenter(view: viewController, dog: dog) 17 | presenter.delegate = delegate 18 | viewController.setPresenter(presenter) 19 | return viewController 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogGallery/DogGalleryContract.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogGalleryContract.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/15/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | protocol DogsGallerySceneDelegate: class { 13 | } 14 | 15 | protocol DogGalleryServerProtocol { 16 | func getDogRandomImageUrl(breed: String, completion: @escaping (String?, Error?) -> Void) 17 | } 18 | 19 | protocol DogGalleryStorageProtocol { 20 | func getFavouriteDogBreed() -> String? 21 | func setFavouriteDogBreed(_ breed: String?) 22 | } 23 | 24 | protocol DogGalleryViewInterface: class { 25 | func setDogImage(_ image: UIImage, animated: Bool) 26 | func setNavigationTitle(_ title: String) 27 | func showHUD(animated: Bool) 28 | func hideHUD(animated: Bool) 29 | func setRightBarButtonItemHighlightState(_ isOn: Bool, animated: Bool) 30 | func setCollectionViewProvider(_ provider: CollectionViewProvider) 31 | func reloadCollectionView() 32 | func showNoDataLabel() 33 | func hideNoDataLabel() 34 | } 35 | 36 | protocol DogGalleryPresentation: class { 37 | func onViewDidLoad() 38 | func handleActionButtonTap() 39 | func handleFavouriteButtonTap() 40 | } 41 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogGallery/DogGalleryFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogGalleryFlow.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 1/17/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum DogGalleryFlow { 12 | enum ViewState { 13 | case loadingRandomImage 14 | case resultRandomImage(UIImage) 15 | case errorGettingRandomImage(message: String) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogGallery/DogGalleryPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogGalleryPresenter.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/15/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class DogGalleryPresenter { 13 | 14 | // MARK: Properties 15 | 16 | weak var delegate: DogsGallerySceneDelegate? 17 | weak var view: DogGalleryViewInterface! 18 | let dog: Dog 19 | let serverService: DogGalleryServerProtocol 20 | let storageService: DogGalleryStorageProtocol 21 | let imageLoader: ImageLoader 22 | var imageLoadingDelayTask: DispatchWorkItem? 23 | lazy var collectionViewProvider = DogBreedsCollectionViewProvider() 24 | var state = DogGalleryFlow.ViewState.loadingRandomImage { 25 | didSet { 26 | updateViewBasedOn(state: state) 27 | } 28 | } 29 | 30 | // MARK: Public methods 31 | 32 | init(view: DogGalleryViewInterface, dog: Dog, serverService: DogGalleryServerProtocol = DogsServerService(), storageService: DogGalleryStorageProtocol = DogsStorageService(), imageLoader: ImageLoader = SimpleImageLoader()) { 33 | self.view = view 34 | self.dog = dog 35 | self.serverService = serverService 36 | self.storageService = storageService 37 | self.imageLoader = imageLoader 38 | } 39 | 40 | func performRequestToGetRandomDogImage(completion: @escaping (_ url: String) -> Void) { 41 | serverService.getDogRandomImageUrl(breed: dog.breed) { [weak self] (urlString, error) in 42 | if let urlString = urlString { 43 | completion(urlString) 44 | } else if let error = error { 45 | self?.state = .errorGettingRandomImage(message: error.localizedDescription) 46 | } 47 | } 48 | } 49 | 50 | func loadRandomDogImage() { 51 | initialiaseImageLoadingDelayTask() 52 | performRequestToGetRandomDogImage { [weak self] (urlString) in 53 | guard let self = self else { return } 54 | self.imageLoadingDelayTask?.cancel() 55 | self.imageLoader.loadImageFrom(urlString: urlString, completion: { [weak self] (image) in 56 | guard let self = self else { return } 57 | if let image = image { 58 | self.state = .resultRandomImage(image) 59 | } else { 60 | self.state = .errorGettingRandomImage(message: "Failed to load image from url") 61 | } 62 | }) 63 | } 64 | } 65 | 66 | func initialiaseImageLoadingDelayTask() { 67 | imageLoadingDelayTask?.cancel() 68 | let task = DispatchWorkItem { [weak self] in 69 | self?.state = .loadingRandomImage 70 | } 71 | imageLoadingDelayTask = task 72 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, execute: task) 73 | } 74 | 75 | func isBreedFavourite() -> Bool { 76 | guard let favouriteBreed = storageService.getFavouriteDogBreed() else { 77 | return false 78 | } 79 | return favouriteBreed == dog.breed 80 | } 81 | 82 | func showDogSubbreeds(_ subbreeds: [String]) { 83 | view.setCollectionViewProvider(collectionViewProvider) 84 | collectionViewProvider.data = subbreeds.map({$0.capitalizingFirstLetter()}) 85 | view.reloadCollectionView() 86 | } 87 | 88 | func updateDataLabelVisibility(hasSubbreeds: Bool) { 89 | if hasSubbreeds { 90 | view.hideNoDataLabel() 91 | } else { 92 | view.showNoDataLabel() 93 | } 94 | } 95 | 96 | func updateRightBarButtonItemHighlightState(_ highlighted: Bool) { 97 | view.setRightBarButtonItemHighlightState(highlighted, animated: true) 98 | } 99 | 100 | func updateViewBasedOn(state: DogGalleryFlow.ViewState) { 101 | switch state { 102 | case .loadingRandomImage: 103 | view.showHUD(animated: true) 104 | case .resultRandomImage(let image): 105 | view.hideHUD(animated: true) 106 | view.setDogImage(image, animated: true) 107 | case .errorGettingRandomImage(_): 108 | view.hideHUD(animated: true) 109 | } 110 | } 111 | 112 | func configureSubbreedsContent(basedOn subbreeds: [String]?) { 113 | if let subbreeds = subbreeds, !subbreeds.isEmpty { 114 | showDogSubbreeds(subbreeds) 115 | view.hideNoDataLabel() 116 | } else { 117 | view.showNoDataLabel() 118 | } 119 | } 120 | } 121 | 122 | // MARK: DogGalleryPresentation Protocol 123 | 124 | extension DogGalleryPresenter: DogGalleryPresentation { 125 | func onViewDidLoad() { 126 | view.setNavigationTitle(dog.breed.capitalizingFirstLetter()) 127 | updateRightBarButtonItemHighlightState(isBreedFavourite()) 128 | configureSubbreedsContent(basedOn: dog.subbreeds) 129 | loadRandomDogImage() 130 | } 131 | 132 | func handleActionButtonTap() { 133 | loadRandomDogImage() 134 | } 135 | 136 | func handleFavouriteButtonTap() { 137 | let valueToStore = isBreedFavourite() ? nil : dog.breed 138 | storageService.setFavouriteDogBreed(valueToStore) 139 | updateRightBarButtonItemHighlightState(isBreedFavourite()) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogGallery/DogGalleryViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogGalleryViewController.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/15/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class DogGalleryViewController: BaseViewController { 13 | 14 | // MARK: Properties 15 | 16 | var presenter: DogGalleryPresentation! 17 | lazy var customView = view as! DogGalleryView 18 | 19 | // MARK: Lifecycle 20 | 21 | override func loadView() { 22 | view = DogGalleryView(frame: UIScreen.main.bounds) 23 | } 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | subscribeOnCustomViewActions() 28 | setupNavigationItem() 29 | presenter.onViewDidLoad() 30 | } 31 | 32 | // MARK: Public methods 33 | 34 | func setPresenter(_ presenter: DogGalleryPresentation) { 35 | self.presenter = presenter 36 | } 37 | 38 | func subscribeOnCustomViewActions() { 39 | customView.didPressActionButton = { [unowned self] in 40 | self.presenter.handleActionButtonTap() 41 | } 42 | } 43 | 44 | func setupNavigationItem() { 45 | let rightBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "pawPrintNotSelected").withRenderingMode(.alwaysTemplate), landscapeImagePhone: nil, style: .done, target: self, action: #selector(favouriteButtonTapped)) 46 | rightBarButtonItem.tintColor = UIColor.AppColors.primaryColor 47 | navigationItem.rightBarButtonItem = rightBarButtonItem 48 | } 49 | 50 | @objc func favouriteButtonTapped() { 51 | presenter.handleFavouriteButtonTap() 52 | } 53 | } 54 | 55 | // MARK: DogGalleryViewInterface 56 | 57 | extension DogGalleryViewController: DogGalleryViewInterface { 58 | func setCollectionViewProvider(_ provider: CollectionViewProvider) { 59 | customView.setCollectionViewProvider(provider) 60 | } 61 | 62 | func reloadCollectionView() { 63 | customView.reloadCollectionView() 64 | } 65 | 66 | func setDogImage(_ image: UIImage, animated: Bool) { 67 | customView.setDogImage(image, animated: animated) 68 | } 69 | 70 | func setNavigationTitle(_ title: String) { 71 | navigationItem.title = title 72 | } 73 | 74 | func showHUD(animated: Bool) { 75 | customView.containerView.showHUD(animated: animated) 76 | } 77 | 78 | func hideHUD(animated: Bool) { 79 | customView.containerView.hideHUD(animated: animated) 80 | } 81 | 82 | func setRightBarButtonItemHighlightState(_ isOn: Bool, animated: Bool) { 83 | guard let rightBarButtonItem = navigationItem.rightBarButtonItem else { 84 | return 85 | } 86 | rightBarButtonItem.image = (isOn ? #imageLiteral(resourceName: "pawPrintSelected") : #imageLiteral(resourceName: "pawPrintNotSelected")).withRenderingMode(.alwaysTemplate) 87 | } 88 | 89 | func showNoDataLabel() { 90 | customView.showNoDataLabel() 91 | } 92 | 93 | func hideNoDataLabel() { 94 | customView.hideNoDataLabel() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogsList/DogsListConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsListConfigurator.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class DogsListConfigurator { 12 | 13 | /// Returns viewController, configured with its associated presenter. 14 | func configuredViewController(delegate: DogsListSceneDelegate?) -> DogsListViewController { 15 | let viewController = DogsListViewController() 16 | let presenter = DogsListPresenter(view: viewController) 17 | presenter.delegate = delegate 18 | viewController.setPresenter(presenter) 19 | return viewController 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogsList/DogsListContract.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsListContract.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | protocol DogsListSceneDelegate: class { 13 | func didSelectDog(_ dog: Dog) 14 | func getGalleryView(for dog: Dog) -> UIViewController 15 | } 16 | 17 | protocol DogsListServerProtocol { 18 | func getAllDogs(completion: @escaping ([Dog]?, Error?) -> Void) 19 | } 20 | 21 | protocol DogsListStorageProtocol { 22 | func getFavouriteDogBreed() -> String? 23 | } 24 | 25 | protocol DogsListViewInterface: class { 26 | func reloadData() 27 | func setTableViewProvider(_ provider: TableViewProvider) 28 | func showAlert(title: String?, message: String?) 29 | func showFavouriteBarButton() 30 | func hideFavouriteBarButton() 31 | } 32 | 33 | protocol DogsListPresentation: class { 34 | func onViewDidLoad() 35 | func onViewWillAppear() 36 | func getGalleryViewForItem(at indexPath: IndexPath) -> UIViewController? 37 | func handleFavouriteButtonTap() 38 | func handleSearchBarTextChange(_ text: String?) 39 | } 40 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogsList/DogsListFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsListFlow.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 1/16/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum DogsListFlow { 12 | enum ViewState { 13 | case loading 14 | case result([Dog]) 15 | case error(message: String) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogsList/DogsListPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsListPresenter.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class DogsListPresenter { 13 | 14 | // MARK: Properties 15 | 16 | weak var delegate: DogsListSceneDelegate? 17 | weak var view: DogsListViewInterface! 18 | let serverService: DogsListServerProtocol 19 | let storageService: DogsListStorageProtocol 20 | let loadingTableViewProvider = LoadingTableViewProvider() 21 | let dogDescriptionFormatter = DogDescriptionFormatter() 22 | lazy var contentTableViewProvider: DogsListTableViewProvider = { 23 | let tableViewProvider = DogsListTableViewProvider() 24 | tableViewProvider.didSelectItem = { [unowned self] (atIndex: Int) in 25 | let dog = self.displayedData[atIndex] 26 | self.delegate?.didSelectDog(dog) 27 | } 28 | return tableViewProvider 29 | }() 30 | // data, fetched from server 31 | var fetchedData = [Dog]() { 32 | willSet { 33 | displayedData = newValue 34 | updateFavouriteButtonVisibility(hasFavouriteDog: isFavouriteDogAvailable) 35 | } 36 | } 37 | var displayedData = [Dog]() { 38 | willSet { 39 | setContentTableView(data: getDogBreedViewModels(from: newValue)) 40 | } 41 | } 42 | var isFavouriteDogAvailable: Bool { 43 | return getFavouriteDog() != nil 44 | } 45 | var state = DogsListFlow.ViewState.loading { 46 | didSet { 47 | updateViewBasedOn(state: state) 48 | } 49 | } 50 | 51 | // MARK: Public methods 52 | 53 | init(view: DogsListViewInterface, serverService: DogsListServerProtocol = DogsServerService(), storageService: DogsListStorageProtocol = DogsStorageService()) { 54 | self.view = view 55 | self.serverService = serverService 56 | self.storageService = storageService 57 | } 58 | 59 | func fetchListOfDogs() { 60 | serverService.getAllDogs { [weak self] (data, error) in 61 | guard let self = self else { return } 62 | if let data = data { 63 | self.state = .result(data) 64 | } else if let error = error { 65 | self.state = .error(message: error.localizedDescription) 66 | } 67 | } 68 | } 69 | 70 | func getDogBreedViewModels(from data: [Dog]) -> [DogBreedViewModel] { 71 | return data.map({ 72 | let description = dogDescriptionFormatter.getBreedDescriptionFrom(dog: $0) 73 | return DogBreedViewModel(title: description.title, subtitle: description.subtitle) 74 | }) 75 | } 76 | 77 | func getFavouriteDog() -> Dog? { 78 | guard let breed = storageService.getFavouriteDogBreed() else { 79 | return nil 80 | } 81 | return fetchedData.filter({$0.breed == breed}).first 82 | } 83 | 84 | func updateFavouriteButtonVisibility(hasFavouriteDog: Bool) { 85 | if hasFavouriteDog { 86 | view.showFavouriteBarButton() 87 | } else { 88 | view.hideFavouriteBarButton() 89 | } 90 | } 91 | 92 | func setLoadingTableView() { 93 | view.setTableViewProvider(loadingTableViewProvider) 94 | view.reloadData() 95 | } 96 | 97 | func setContentTableView(data: [DogBreedViewModel]) { 98 | view.setTableViewProvider(contentTableViewProvider) 99 | updateContentDataInView(data: data) 100 | } 101 | 102 | func updateContentDataInView(data: [DogBreedViewModel]) { 103 | contentTableViewProvider.data = data 104 | view.reloadData() 105 | } 106 | 107 | func updateViewBasedOn(state: DogsListFlow.ViewState) { 108 | switch state { 109 | case .loading: 110 | setLoadingTableView() 111 | // adding delay due to imitation of heavy request 112 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { 113 | self.fetchListOfDogs() 114 | } 115 | case .result(let items): 116 | fetchedData = items 117 | case .error(let message): 118 | view.showAlert(title: nil, message: message) 119 | } 120 | } 121 | } 122 | 123 | // MARK: DogsListPresentation Protocol 124 | 125 | extension DogsListPresenter: DogsListPresentation { 126 | func onViewDidLoad() { 127 | state = .loading 128 | } 129 | 130 | func onViewWillAppear() { 131 | updateFavouriteButtonVisibility(hasFavouriteDog: isFavouriteDogAvailable) 132 | } 133 | 134 | func handleFavouriteButtonTap() { 135 | if let dog = getFavouriteDog() { 136 | delegate?.didSelectDog(dog) 137 | } 138 | } 139 | 140 | func getGalleryViewForItem(at indexPath: IndexPath) -> UIViewController? { 141 | let dog = displayedData[indexPath.row] 142 | return delegate?.getGalleryView(for: dog) 143 | } 144 | 145 | func handleSearchBarTextChange(_ text: String?) { 146 | guard let text = text, !text.isEmpty else { 147 | displayedData = fetchedData 148 | return 149 | } 150 | displayedData = fetchedData.filter({$0.breed.lowercased().hasPrefix(text.lowercased())}) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogsList/DogsListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsListView.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DogsListView: UIView { 12 | 13 | // MARK: Properties 14 | 15 | let tableView: UITableView = { 16 | let tableView = UITableView(frame: .zero, style: .plain) 17 | tableView.translatesAutoresizingMaskIntoConstraints = false 18 | tableView.rowHeight = 70 19 | return tableView 20 | }() 21 | 22 | // MARK: Init 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | backgroundColor = .white 27 | addTableView() 28 | registerTableViewCells() 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | // MARK: Methods 36 | 37 | private func addTableView() { 38 | addSubview(tableView) 39 | NSLayoutConstraint(item: tableView, attribute: .top, relatedBy: .equal, toItem: safeAreaLayoutGuide, attribute: .top, multiplier: 1.0, constant: 0).isActive = true 40 | NSLayoutConstraint(item: tableView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true 41 | NSLayoutConstraint(item: tableView, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: 0).isActive = true 42 | NSLayoutConstraint(item: tableView, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: 0).isActive = true 43 | } 44 | 45 | private func registerTableViewCells() { 46 | tableView.register(DogBreedTableViewCell.self, forCellReuseIdentifier: DogBreedTableViewCell.reuseIdentifier) 47 | tableView.register(LoadingTableViewCell.self, forCellReuseIdentifier: LoadingTableViewCell.reuseIdentifier) 48 | } 49 | } 50 | 51 | // MARK: DogsListView public methods 52 | 53 | extension DogsListView { 54 | func reloadDataInTableView() { 55 | tableView.reloadData() 56 | } 57 | 58 | func setTableViewProvider(_ provider: TableViewProvider) { 59 | tableView.delegate = provider 60 | tableView.dataSource = provider 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogsList/DogsListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsListViewController.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class DogsListViewController: BaseViewController { 13 | 14 | // MARK: Properties 15 | 16 | var presenter: DogsListPresentation! 17 | lazy var customView = view as! DogsListView 18 | var keyboardObserver: KeyboardObserver? 19 | 20 | // MARK: Lifecycle 21 | 22 | override func loadView() { 23 | view = DogsListView(frame: .zero) 24 | } 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | keyboardObserver = KeyboardObserver(targetView: customView.tableView) 29 | setupSearchController() 30 | setupNavigationItem() 31 | registerTableViewForForceTouchInteractions() 32 | showNavigationBar() 33 | presenter.onViewDidLoad() 34 | } 35 | 36 | override func viewWillAppear(_ animated: Bool) { 37 | super.viewWillAppear(animated) 38 | presenter.onViewWillAppear() 39 | } 40 | 41 | // MARK: Public methods 42 | 43 | func setPresenter(_ presenter: DogsListPresentation) { 44 | self.presenter = presenter 45 | } 46 | 47 | func registerTableViewForForceTouchInteractions() { 48 | if traitCollection.forceTouchCapability == .available { 49 | registerForPreviewing(with: self, sourceView: customView.tableView) 50 | } 51 | } 52 | 53 | func setupSearchController() { 54 | let searchController = UISearchController(searchResultsController: nil) 55 | searchController.searchResultsUpdater = self 56 | searchController.dimsBackgroundDuringPresentation = false 57 | navigationItem.searchController = searchController 58 | definesPresentationContext = true 59 | } 60 | 61 | func setupNavigationItem() { 62 | navigationItem.title = "List of dogs" 63 | } 64 | 65 | @objc func favouriteButtonTapped(_ sender: Any) { 66 | presenter.handleFavouriteButtonTap() 67 | } 68 | } 69 | 70 | extension DogsListViewController: UISearchResultsUpdating { 71 | func updateSearchResults(for searchController: UISearchController) { 72 | presenter.handleSearchBarTextChange(searchController.searchBar.text) 73 | } 74 | } 75 | 76 | extension DogsListViewController: UIViewControllerPreviewingDelegate { 77 | func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { 78 | guard let tableView = previewingContext.sourceView as? UITableView else { 79 | return nil 80 | } 81 | guard let indexPath = tableView.indexPathForRow(at: location) else { 82 | return nil 83 | } 84 | previewingContext.sourceRect = tableView.rectForRow(at: indexPath) 85 | return presenter.getGalleryViewForItem(at: indexPath) 86 | } 87 | 88 | func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { 89 | if let viewController = (viewControllerToCommit as? UINavigationController)?.viewControllers[0] { 90 | navigationController?.pushViewController(viewController, animated: true) 91 | } 92 | } 93 | } 94 | 95 | // MARK: DogsListViewInterface 96 | 97 | extension DogsListViewController: DogsListViewInterface { 98 | func reloadData() { 99 | customView.reloadDataInTableView() 100 | } 101 | 102 | func setTableViewProvider(_ provider: TableViewProvider) { 103 | customView.setTableViewProvider(provider) 104 | } 105 | 106 | func showAlert(title: String?, message: String?) { 107 | presentAlert(title: title, message: message) 108 | } 109 | 110 | func showFavouriteBarButton() { 111 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Favourite", style: .done, target: self, action: #selector(favouriteButtonTapped(_:))) 112 | } 113 | 114 | func hideFavouriteBarButton() { 115 | navigationItem.rightBarButtonItem = nil 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogsList/TableView/DogBreedTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogBreedTableViewCell.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DogBreedTableViewCell: UITableViewCell { 12 | 13 | // MARK: Private properties 14 | 15 | private var titleLabel: UILabel! 16 | private var subtitleLabel: UILabel! 17 | private var iconImageView: UIImageView! 18 | private var iconImageViewContainer: UIView! 19 | 20 | // MARK: Init 21 | 22 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 23 | super.init(style: style, reuseIdentifier: reuseIdentifier) 24 | selectionStyle = .none 25 | addIconImageView() 26 | addTitleLabel() 27 | addSubtitleLabel() 28 | } 29 | 30 | required init?(coder aDecoder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | override func setHighlighted(_ highlighted: Bool, animated: Bool) { 35 | if highlighted { 36 | contentView.backgroundColor = UIColor.black.withAlphaComponent(0.1) 37 | } else { 38 | contentView.backgroundColor = .clear 39 | } 40 | } 41 | 42 | // MARK: Private methods 43 | 44 | private func addIconImageView() { 45 | let size: CGFloat = 50 46 | let iconImageViewContainer = UIView() 47 | iconImageViewContainer.translatesAutoresizingMaskIntoConstraints = false 48 | iconImageViewContainer.backgroundColor = UIColor.AppColors.primaryColor 49 | iconImageViewContainer.clipsToBounds = true 50 | iconImageViewContainer.layer.cornerRadius = size / 2 51 | self.iconImageViewContainer = iconImageViewContainer 52 | contentView.addSubview(iconImageViewContainer) 53 | NSLayoutConstraint(item: iconImageViewContainer, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1.0, constant: 10).isActive = true 54 | NSLayoutConstraint(item: iconImageViewContainer, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1.0, constant: -10).isActive = true 55 | NSLayoutConstraint(item: iconImageViewContainer, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1.0, constant: 16).isActive = true 56 | NSLayoutConstraint(item: iconImageViewContainer, attribute: .width, relatedBy: .equal, toItem: iconImageViewContainer, attribute: .height, multiplier: 1.0, constant: 0).isActive = true 57 | let iconImageView = UIImageView(image: #imageLiteral(resourceName: "pawPrint").withRenderingMode(.alwaysTemplate)) 58 | iconImageView.tintColor = UIColor.AppColors.secondaryColor 59 | iconImageView.translatesAutoresizingMaskIntoConstraints = false 60 | iconImageView.clipsToBounds = true 61 | self.iconImageView = iconImageView 62 | iconImageViewContainer.addSubview(iconImageView) 63 | NSLayoutConstraint(item: iconImageView, attribute: .top, relatedBy: .equal, toItem: iconImageViewContainer, attribute: .top, multiplier: 1.0, constant: 10).isActive = true 64 | NSLayoutConstraint(item: iconImageView, attribute: .leading, relatedBy: .equal, toItem: iconImageViewContainer, attribute: .leading, multiplier: 1.0, constant: 10).isActive = true 65 | NSLayoutConstraint(item: iconImageView, attribute: .bottom, relatedBy: .equal, toItem: iconImageViewContainer, attribute: .bottom, multiplier: 1.0, constant: -10).isActive = true 66 | NSLayoutConstraint(item: iconImageView, attribute: .trailing, relatedBy: .equal, toItem: iconImageViewContainer, attribute: .trailing, multiplier: 1.0, constant: -10).isActive = true 67 | } 68 | 69 | private func addTitleLabel() { 70 | let titleLabel = UILabel(frame: .zero) 71 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 72 | titleLabel.font = UIFont.systemFont(ofSize: 18, weight: .bold) 73 | self.titleLabel = titleLabel 74 | contentView.addSubview(titleLabel) 75 | NSLayoutConstraint(item: titleLabel, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1.0, constant: 10).isActive = true 76 | NSLayoutConstraint(item: titleLabel, attribute: .leading, relatedBy: .equal, toItem: iconImageViewContainer, attribute: .trailing, multiplier: 1.0, constant: 12).isActive = true 77 | NSLayoutConstraint(item: titleLabel, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1.0, constant: -10).isActive = true 78 | } 79 | 80 | private func addSubtitleLabel() { 81 | let subtitleLabel = UILabel(frame: .zero) 82 | subtitleLabel.translatesAutoresizingMaskIntoConstraints = false 83 | subtitleLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium) 84 | self.subtitleLabel = subtitleLabel 85 | contentView.addSubview(subtitleLabel) 86 | NSLayoutConstraint(item: subtitleLabel, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1.0, constant: -10).isActive = true 87 | NSLayoutConstraint(item: subtitleLabel, attribute: .leading, relatedBy: .equal, toItem: iconImageViewContainer, attribute: .trailing, multiplier: 1.0, constant: 12).isActive = true 88 | NSLayoutConstraint(item: subtitleLabel, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1.0, constant: -10).isActive = true 89 | } 90 | } 91 | 92 | // MARK: Public methods 93 | 94 | extension DogBreedTableViewCell { 95 | func setTitle(_ title: String, subtitle: String) { 96 | titleLabel.text = title 97 | subtitleLabel.text = subtitle 98 | } 99 | 100 | func getTitle() -> String? { 101 | return titleLabel.text 102 | } 103 | 104 | func getSubtitle() -> String? { 105 | return subtitleLabel.text 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/DogsFlow/DogsList/TableView/DogsListTableViewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsListTableViewProvider.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class DogsListTableViewProvider: NSObject, TableViewProvider { 13 | 14 | // MARK: Properties 15 | 16 | var data = [DogBreedViewModel]() 17 | var didSelectItem: ((_ atIndex: Int) -> Void)? 18 | 19 | // MARK: TableViewProvider methods 20 | 21 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 22 | return data.count 23 | } 24 | 25 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 26 | guard let cell = tableView.dequeueReusableCell(withIdentifier: DogBreedTableViewCell.reuseIdentifier, for: indexPath) as? DogBreedTableViewCell else { 27 | return UITableViewCell() 28 | } 29 | let viewModel = data[indexPath.row] 30 | cell.setTitle(viewModel.title, subtitle: viewModel.subtitle) 31 | return cell 32 | } 33 | 34 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 35 | tableView.deselectRow(at: indexPath, animated: true) 36 | didSelectItem?(indexPath.row) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/Start/StartConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartConfigurator.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class StartConfigurator { 12 | 13 | /// Returns viewController, configured with its associated presenter. 14 | func configuredViewController(delegate: StartPresenterDelegate?) -> StartViewController { 15 | let viewController = StartViewController() 16 | let presenter = StartPresenter(view: viewController, userCredentialsStorage: UserCredentialsStorageService(storage: UserDefaultsLayer())) 17 | presenter.delegate = delegate 18 | viewController.setPresenter(presenter) 19 | return viewController 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/Start/StartContract.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartContract.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol StartViewInterface: class { 12 | func showHUD(animated: Bool) 13 | func hideHUD(animated: Bool) 14 | } 15 | 16 | protocol StartPresentation: class { 17 | func onViewDidLoad() 18 | } 19 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/Start/StartPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartPresenter.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol StartPresenterDelegate: class { 12 | func userNeedsToAuthenticate() 13 | func userIsAuthenticated() 14 | } 15 | 16 | class StartPresenter { 17 | 18 | // MARK: Properties 19 | 20 | weak var delegate: StartPresenterDelegate? 21 | weak var view: StartViewInterface! 22 | let userCredentialsStorage: UserCredentialsStorageService 23 | 24 | // MARK: Public methods 25 | 26 | init(view: StartViewInterface, userCredentialsStorage: UserCredentialsStorageService) { 27 | self.view = view 28 | self.userCredentialsStorage = userCredentialsStorage 29 | } 30 | 31 | func checkUserState() { 32 | view.showHUD(animated: true) 33 | // could be some url request here 34 | // immitation via 'asyncAfter'. 35 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) { [weak self] in 36 | guard let self = self else { return } 37 | // handle of result 38 | self.view.hideHUD(animated: true) 39 | if self.userCredentialsStorage.isUserAuthenticated { // user is authenticated 40 | self.delegate?.userIsAuthenticated() 41 | } else { // user needs to authenticate 42 | self.delegate?.userNeedsToAuthenticate() 43 | } 44 | } 45 | } 46 | } 47 | 48 | // MARK: StartPresentation Protocol 49 | 50 | extension StartPresenter: StartPresentation { 51 | func onViewDidLoad() { 52 | checkUserState() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/Start/StartView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartView.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class StartView: UIView { 12 | 13 | // MARK: Init 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | backgroundColor = .white 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NiceDemo/Scenes/Start/StartViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartViewController.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class StartViewController: BaseViewController { 13 | 14 | // MARK: Properties 15 | 16 | lazy var customView = view as! StartView 17 | var presenter: StartPresentation! 18 | 19 | // MARK: Lifecycle 20 | 21 | override func loadView() { 22 | self.view = StartView(frame: UIScreen.main.bounds) 23 | } 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | hideNavigationBar() 28 | presenter.onViewDidLoad() 29 | } 30 | 31 | // MARK: Public methods 32 | 33 | func setPresenter(_ presenter: StartPresentation) { 34 | self.presenter = presenter 35 | } 36 | } 37 | 38 | // MARK: StartViewInterface 39 | 40 | extension StartViewController: StartViewInterface { 41 | func showHUD(animated: Bool) { 42 | customView.showHUD(backgroundColor: .white, tintColor: .black, animated: true) 43 | } 44 | 45 | func hideHUD(animated: Bool) { 46 | customView.hideHUD(animated: animated) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /NiceDemo/Services/AppDelegateService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegateService.swift 3 | // 4 | // 5 | // Created by Serhii Kharauzov on 1/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | /// Responsible for handling all business logic, that should be established at AppDelegate. 13 | class AppDelegateService { 14 | 15 | // MARK: Public properties 16 | 17 | /// root coordinator 18 | var appCoordinator: AppCoordinator! 19 | /// root window of the app 20 | let window: UIWindow 21 | 22 | // MARK: Init 23 | 24 | init(window: UIWindow) { 25 | self.window = window 26 | } 27 | 28 | // MARK: Public methods 29 | 30 | func setupAppCoordinator() { 31 | window.rootViewController = BaseNavigationController() 32 | guard let rootNavigationController = window.rootViewController as? UINavigationController else { 33 | fatalError("Root viewController must be inherited from UINavigationController") 34 | } 35 | appCoordinator = AppCoordinator(navigationController: rootNavigationController) 36 | appCoordinator.start() 37 | window.makeKeyAndVisible() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /NiceDemo/Services/KeyboardObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardObserver.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 4/4/19. 6 | // Copyright © 2019 NiceDemo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | /// Responsible for observing keyboard notifications like `UIKeyboardWillShow` or `UIKeyboardWillHide` 13 | /// inside target(UIViewController, UIView etc.) and adopt target's view to fit 14 | /// the screen, when keyboard is visible or not. 15 | class KeyboardObserver { 16 | 17 | let targetView: UIScrollView? 18 | var keyboardWillShowHandler: ((_ notification: NSNotification) -> Void)? 19 | var keyboardWillHideHandler: ((_ notification: NSNotification) -> Void)? 20 | 21 | init(targetView: UIScrollView?) { 22 | self.targetView = targetView 23 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) 24 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) 25 | } 26 | 27 | deinit { 28 | NotificationCenter.default.removeObserver(self) 29 | } 30 | 31 | @objc func keyboardWillShow(notification: NSNotification) { 32 | guard let userInfo = notification.userInfo else { return } 33 | guard let keyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return } 34 | targetView?.contentInset.bottom = keyboardFrame.size.height 35 | keyboardWillShowHandler?(notification) 36 | } 37 | 38 | @objc func keyboardWillHide(notification: NSNotification) { 39 | targetView?.contentInset = .zero 40 | keyboardWillHideHandler?(notification) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /NiceDemo/Services/LocalStorage/DogsStorageService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsStorageService.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol DogsStorage { 12 | var favouriteDogBreed: String? { get set } 13 | } 14 | 15 | extension DogsStorageService: DogGalleryStorageProtocol, DogsListStorageProtocol {} 16 | 17 | /// We used 'storage' property, that is abstract class for any data storage, that could 18 | /// be used for setting/retrieving data. One can replace it on any class, that implements 19 | /// 'DogsStorage' protocol. For now we use UserDefaults as a simple example, but 20 | /// it can be easily replaced on SQLite, Realm, CoreData etc. 21 | class DogsStorageService { 22 | 23 | private var storage: DogsStorage 24 | 25 | init(storage: DogsStorage = UserDefaultsLayer()) { 26 | self.storage = storage 27 | } 28 | 29 | // MARK: Public properties 30 | 31 | func getFavouriteDogBreed() -> String? { 32 | return storage.favouriteDogBreed 33 | } 34 | 35 | func setFavouriteDogBreed(_ breed: String?) { 36 | storage.favouriteDogBreed = breed 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NiceDemo/Services/LocalStorage/UserCredentialsStorageService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserCredentialsStorageService.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/5/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol UserCredentialsStorage { 12 | var isUserAuthenticated: Bool { get set } 13 | } 14 | 15 | /// Responsible for keeping user's credentials data. 16 | /// If data supposed to be sensitive, please use Keychain for that. 17 | /// We used 'storage' property, that is abstract class for any data storage, that could 18 | /// be used for setting/retrieving data. One can replace it on any class, that implements 19 | /// 'UserCredentialsStorage' protocol. For now we use UserDefaults as a simple example, but 20 | /// it can be easily replaced on SQLite, Realm, CoreData etc. 21 | class UserCredentialsStorageService { 22 | 23 | private var storage: UserCredentialsStorage 24 | 25 | init(storage: UserCredentialsStorage = UserDefaultsLayer()) { 26 | self.storage = storage 27 | } 28 | 29 | // MARK: Public properties 30 | 31 | var isUserAuthenticated: Bool { 32 | get { 33 | return storage.isUserAuthenticated 34 | } set { 35 | storage.isUserAuthenticated = newValue 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NiceDemo/Services/ServerApi/Core/ServerRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerRouter.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/13/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Types adopting the `URLRequestable` protocol can be used to construct URL requests. 12 | protocol URLRequestable { 13 | /// Returns a URL request. 14 | func asURLRequest() -> URLRequest 15 | } 16 | 17 | /// A dictionary of parameters to apply to a `URLRequest`. 18 | typealias URLRequestParameters = [String: Any] 19 | 20 | /// Base HTTP method definitions. 21 | enum HTTPMethod: String { 22 | case get = "GET" 23 | case head = "HEAD" 24 | case post = "POST" 25 | case put = "PUT" 26 | case patch = "PATCH" 27 | case delete = "DELETE" 28 | } 29 | 30 | /// `URLRequest` builder, that could be used for any HTTP(s) wrapper, 31 | /// that supports `URLRequestable` protocol. 32 | enum ServerRouter: URLRequestable { 33 | 34 | // MARK: Requests 35 | 36 | case getAllDogsBreeds 37 | case getDogRandomImage(String) 38 | 39 | // MARK: Interface 40 | 41 | /// Base url string for all endpoints in the project. 42 | static let baseURLString = "https://dog.ceo/api/" 43 | 44 | /// URLRequest builder method. 45 | func asURLRequest() -> URLRequest { 46 | var method: HTTPMethod { 47 | switch self { 48 | case .getAllDogsBreeds, .getDogRandomImage: 49 | return .get 50 | } 51 | } 52 | var params: (URLRequestParameters?) { 53 | switch self { 54 | case .getAllDogsBreeds, .getDogRandomImage: 55 | return nil 56 | } 57 | } 58 | let url: URL = { 59 | let relativePath: String 60 | switch self { 61 | case .getAllDogsBreeds: 62 | relativePath = "breeds/list/all" 63 | case .getDogRandomImage(let string): 64 | relativePath = "breed/\(string)/images/random" 65 | } 66 | return URL(string: ServerRouter.baseURLString)!.appendingPathComponent(relativePath) 67 | }() 68 | var urlRequest = URLRequest(url: url) 69 | urlRequest.httpMethod = method.rawValue 70 | if let unwrappedParams = params { // add parameters to httpBody if needed 71 | let jsonData = try? JSONSerialization.data(withJSONObject: unwrappedParams, options: .prettyPrinted) 72 | urlRequest.httpBody = jsonData 73 | } 74 | return urlRequest 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /NiceDemo/Services/ServerApi/Core/UrlSessionService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UrlSessionService.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/14/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Responsible for performing network requests, using `URLSession`. 12 | /// 13 | /// You shouldn't call its methods explicitly! Please use it as an abstract layer in 14 | /// server services via protocols. 15 | class UrlSessionService: ServerService { 16 | 17 | // MARK: Private properties 18 | 19 | private let session: URLSession 20 | 21 | // MARK: Public methods 22 | 23 | init(session: URLSession = URLSession(configuration: .default)) { 24 | self.session = session 25 | } 26 | 27 | func performRequest(_ request: URLRequestable, completion: ServerRequestResponseCompletion?) { 28 | session.dataTask(with: request.asURLRequest()) { (data, response, error) in 29 | let result: Result 30 | defer { 31 | DispatchQueue.main.async { 32 | completion?(result) 33 | } 34 | } 35 | if let response = response as? HTTPURLResponse, 200...299 ~= response.statusCode { 36 | guard let data = data as? T else { return result = .failure(HTTPRequestError.invalidResponseDataType) } 37 | result = .success(data) 38 | } else { 39 | result = .failure(self.getFormattedError(responseData: data, defaultError: error)) 40 | } 41 | }.resume() 42 | } 43 | 44 | // MARK: Private methods 45 | 46 | private func getFormattedError(responseData: Data?, defaultError: Error?) -> Error { 47 | if let data = responseData { 48 | if let baseResponse = try? JSONDecoder().decode(BaseResponse.self, from: data), 49 | let errorString = baseResponse.error { 50 | return HTTPRequestError.custom(errorString) 51 | } 52 | } 53 | return defaultError ?? HTTPRequestError.invalidResponse 54 | } 55 | } 56 | 57 | extension UrlSessionService { 58 | enum HTTPRequestError: Error { 59 | case invalidResponseDataType 60 | case invalidResponse 61 | case custom(String) 62 | 63 | var localizedDescription: String { 64 | switch self { 65 | case .invalidResponseDataType: 66 | return "Response's data format is different from expected." 67 | case .invalidResponse: 68 | return "Invalid response." 69 | case .custom(let value): 70 | return value 71 | } 72 | } 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /NiceDemo/Services/ServerApi/DogsApiDataParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsApiDataParser.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 18.04.2020. 6 | // Copyright © 2020 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class DogsApiDataParser: DogsServerResponseParser { 12 | 13 | private let decoder = JSONDecoder() 14 | 15 | func parseGetAllDogsResponse(data: Data) -> [Dog]? { 16 | do { 17 | let parsedResponse = try decoder.decode(GetAllDogsServerResponse.self, from: data) 18 | return parsedResponse.formattedData 19 | } catch { 20 | return nil 21 | } 22 | } 23 | 24 | func parseGetDogRandomImageUrlResponse(data: Data) -> String? { 25 | do { 26 | let parsedResponse = try decoder.decode(GetRandomDogImageServerResponse.self, from: data) 27 | return parsedResponse.data 28 | } catch { 29 | return nil 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /NiceDemo/Services/ServerApi/DogsServerService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsServerService.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/13/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol DogsServerResponseParser { 12 | func parseGetAllDogsResponse(data: Data) -> [Dog]? 13 | func parseGetDogRandomImageUrlResponse(data: Data) -> String? 14 | } 15 | 16 | /// Responsible for performing network requests relating to Dogs. 17 | /// 18 | /// We used *core* property, that is abstract class for any network manager, that could 19 | /// be used for implementing network requests. One can replace it on any class, that implements 20 | /// **ServerService** protocol. For now we use `UrlSession` as a simple example, but 21 | /// it can be easily replaced on `Alamofire`, `AFNetworking`, etc. 22 | class DogsServerService { 23 | 24 | // MARK: Private properties 25 | 26 | private let core: ServerService 27 | private let parser: DogsServerResponseParser 28 | 29 | // MARK: Public methods 30 | 31 | init(core: ServerService = UrlSessionService(), parser: DogsServerResponseParser = DogsApiDataParser()) { 32 | self.core = core 33 | self.parser = parser 34 | } 35 | 36 | func getAllDogs(completion: @escaping ([Dog]?, Error?) -> Void) { 37 | core.performRequest(ServerRouter.getAllDogsBreeds) { (result: Result) in 38 | switch result { 39 | case .success(let data): 40 | completion(self.parser.parseGetAllDogsResponse(data: data), nil) 41 | case .failure(let error): 42 | completion(nil, error) 43 | } 44 | } 45 | } 46 | 47 | func getDogRandomImageUrl(breed: String, completion: @escaping (String?, Error?) -> Void) { 48 | core.performRequest(ServerRouter.getDogRandomImage(breed)) { (result: Result) in 49 | switch result { 50 | case .success(let data): 51 | completion(self.parser.parseGetDogRandomImageUrlResponse(data: data), nil) 52 | case .failure(let error): 53 | completion(nil, error) 54 | } 55 | } 56 | } 57 | } 58 | 59 | extension DogsServerService: DogGalleryServerProtocol, DogsListServerProtocol {} 60 | 61 | -------------------------------------------------------------------------------- /NiceDemo/Services/SimpleImageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleImageLoader.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/15/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | protocol ImageLoader { 13 | func loadImageFrom(urlString: String, completion: @escaping (UIImage?) -> Void) 14 | } 15 | 16 | protocol ImageLoaderCore { 17 | func getImageData(from url: URL, completion: @escaping (_ data: Data?) -> Void) 18 | } 19 | 20 | /// Responsible for retrieving UIImage via url using `Data.contentsOfUrl` method. 21 | /// 22 | /// __DISCLAIMER__: This class was created just for demo purpose, where we need single loading at a time. It does not provide neither smart multi-loading nor caching. 23 | /// 24 | /// Please use another services like Kingfisher, Haneke etc. for such purposes. 25 | class SimpleImageLoader: ImageLoader { 26 | 27 | let core: ImageLoaderCore 28 | 29 | init(core: ImageLoaderCore = SimpleImageLoaderCore()) { 30 | self.core = core 31 | } 32 | 33 | func loadImageFrom(urlString: String, completion: @escaping (UIImage?) -> Void) { 34 | guard let url = URL(string: urlString) else { 35 | completion(nil) 36 | return 37 | } 38 | core.getImageData(from: url) { (data) in 39 | if let data = data { 40 | completion(UIImage(data: data)) 41 | } else { 42 | completion(nil) 43 | } 44 | } 45 | } 46 | } 47 | 48 | class SimpleImageLoaderCore: ImageLoaderCore { 49 | func getImageData(from url: URL, completion: @escaping (_ data: Data?) -> Void) { 50 | DispatchQueue.global(qos: .background).async { 51 | let data = try? Data(contentsOf: url) 52 | DispatchQueue.main.async { 53 | completion(data) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /NiceDemo/Services/UserDefaultsLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsLayer.swift 3 | // 4 | // 5 | // Created by Serhii Kharauzov on 1/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Wrapper, responsible for storing/retrieving data using UserDefaults. 12 | /// 13 | /// You shouldn't call its properties explicitly! Please use it as an abstract layer in 14 | /// storage services via protocols. 15 | class UserDefaultsLayer: UserCredentialsStorage, DogsStorage { 16 | 17 | init(userDefaultsSuiteName: String? = nil) { 18 | defaults = UserDefaults(suiteName: userDefaultsSuiteName)! 19 | } 20 | 21 | // MARK: Public Properties 22 | 23 | var isUserAuthenticated: Bool { 24 | get { 25 | return defaults.bool(forKey: GeneralKeys.isUserAuthenticated) 26 | } set { 27 | defaults.set(newValue, forKey: GeneralKeys.isUserAuthenticated) 28 | } 29 | } 30 | 31 | var favouriteDogBreed: String? { 32 | get { 33 | return defaults.string(forKey: GeneralKeys.favouriteDogBreed) 34 | } set { 35 | defaults.set(newValue, forKey: GeneralKeys.favouriteDogBreed) 36 | } 37 | } 38 | 39 | // MARK: Private properties 40 | 41 | private let defaults: UserDefaults 42 | } 43 | 44 | extension UserDefaultsLayer { 45 | private struct GeneralKeys { 46 | static let isUserAuthenticated = "isUserAuthenticated" 47 | static let favouriteDogBreed = "favouriteDogBreed" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /NiceDemo/Services/Validator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Validator.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/7/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Responsible to validate different data. 12 | class Validator { 13 | 14 | // MARK: Public methods 15 | 16 | func validateEmail(_ value: String?) throws { 17 | guard let email = value else { 18 | throw ValidationError.valueIsEmpty("Email") 19 | } 20 | if email.isEmpty { 21 | throw ValidationError.valueIsEmpty("Email") 22 | } else if !isValidEmail(email) { 23 | throw ValidationError.emailFormatIsWrong 24 | } 25 | } 26 | 27 | func validatePassword(_ value: String?) throws { 28 | guard let password = value else { 29 | throw ValidationError.valueIsEmpty("Password") 30 | } 31 | if password.isEmpty { 32 | throw ValidationError.valueIsEmpty("Password") 33 | } else if password.count < Constants.passwordRequiredLength { 34 | throw ValidationError.passwordLengthIsWrong 35 | } 36 | } 37 | 38 | // MARK: Private methods 39 | 40 | private func isValidEmail(_ email: String) -> Bool { 41 | let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}" 42 | let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegex) 43 | return emailTest.evaluate(with: email) 44 | } 45 | } 46 | 47 | extension Validator { 48 | /// Represents error type with detailed description. 49 | enum ValidationError: Error, Equatable { 50 | case emailFormatIsWrong 51 | case emailFormatIsDublicated 52 | case passwordLengthIsWrong 53 | case valueIsEmpty(String) 54 | case unknown 55 | 56 | var localizedDescription: String { 57 | switch self { 58 | case .emailFormatIsWrong: 59 | return "Email has wrong format. Please edit it." 60 | case .emailFormatIsDublicated: 61 | return "This email is already used by other user. Please try another one." 62 | case .passwordLengthIsWrong: 63 | return "Password must have minimum \(Constants.passwordRequiredLength) symbols." 64 | case .valueIsEmpty(let value): 65 | return "\(value) is empty. Please fill it." 66 | default: 67 | return "Unknown error occured." 68 | } 69 | } 70 | 71 | static func ==(lhs: ValidationError, rhs: ValidationError) -> Bool { 72 | switch (lhs, rhs) { 73 | case (.emailFormatIsWrong, .emailFormatIsWrong): return true 74 | case (.emailFormatIsDublicated, emailFormatIsDublicated): return true 75 | case (.passwordLengthIsWrong, .passwordLengthIsWrong): return true 76 | case let (.valueIsEmpty(l), .valueIsEmpty(r)): return l == r 77 | case (.unknown, .unknown) : return true 78 | default: return false 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "57x57", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-57x57@1x.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "57x57", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-57x57@2x.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "60x60", 59 | "idiom" : "iphone", 60 | "filename" : "Icon-App-60x60@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "60x60", 65 | "idiom" : "iphone", 66 | "filename" : "Icon-App-60x60@3x.png", 67 | "scale" : "3x" 68 | }, 69 | { 70 | "size" : "20x20", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-20x20@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "20x20", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-20x20@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "29x29", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-29x29@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "29x29", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-29x29@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "40x40", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-40x40@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "40x40", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-40x40@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "50x50", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-Small-50x50@1x.png", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "size" : "50x50", 113 | "idiom" : "ipad", 114 | "filename" : "Icon-Small-50x50@2x.png", 115 | "scale" : "2x" 116 | }, 117 | { 118 | "size" : "72x72", 119 | "idiom" : "ipad", 120 | "filename" : "Icon-App-72x72@1x.png", 121 | "scale" : "1x" 122 | }, 123 | { 124 | "size" : "72x72", 125 | "idiom" : "ipad", 126 | "filename" : "Icon-App-72x72@2x.png", 127 | "scale" : "2x" 128 | }, 129 | { 130 | "size" : "76x76", 131 | "idiom" : "ipad", 132 | "filename" : "Icon-App-76x76@1x.png", 133 | "scale" : "1x" 134 | }, 135 | { 136 | "size" : "76x76", 137 | "idiom" : "ipad", 138 | "filename" : "Icon-App-76x76@2x.png", 139 | "scale" : "2x" 140 | }, 141 | { 142 | "size" : "83.5x83.5", 143 | "idiom" : "ipad", 144 | "filename" : "Icon-App-83.5x83.5@2x.png", 145 | "scale" : "2x" 146 | }, 147 | { 148 | "size" : "1024x1024", 149 | "idiom" : "ios-marketing", 150 | "filename" : "ItunesArtwork@2x.png", 151 | "scale" : "1x" 152 | } 153 | ], 154 | "info" : { 155 | "version" : 1, 156 | "author" : "xcode" 157 | } 158 | } -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/dog.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/dog.imageset/dog.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/dog.imageset/dog.pdf -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/pawPrint.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pawPrint.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/pawPrint.imageset/pawPrint.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/pawPrint.imageset/pawPrint.pdf -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/pawPrintNotSelected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pawPrintNotSelected.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/pawPrintNotSelected.imageset/pawPrintNotSelected.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/pawPrintNotSelected.imageset/pawPrintNotSelected.pdf -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/pawPrintSelected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pawPrintSelected.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/pawPrintSelected.imageset/pawPrintSelected.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/pawPrintSelected.imageset/pawPrintSelected.pdf -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/walkingWithDog.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "walkingWithDog.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Assets.xcassets/walkingWithDog.imageset/walkingWithDog.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/NiceDemo/SupportingFiles/Assets.xcassets/walkingWithDog.imageset/walkingWithDog.pdf -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /NiceDemo/SupportingFiles/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | 33 | UISupportedInterfaceOrientations~ipad 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationPortraitUpsideDown 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | UIUserInterfaceStyle 41 | Light 42 | 43 | 44 | -------------------------------------------------------------------------------- /NiceDemo/SystemFiles/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/4/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | // MARK: Properties 14 | 15 | var window: UIWindow? 16 | var appDelegateService: AppDelegateService! 17 | 18 | // MARK: UIApplication Delegate 19 | 20 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 21 | let window = UIWindow(frame: UIScreen.main.bounds) 22 | appDelegateService = AppDelegateService(window: window) 23 | appDelegateService.setupAppCoordinator() 24 | self.window = window 25 | return true 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /NiceDemo/SystemFiles/TestAppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestAppDelegate.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/21/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TestAppDelegate: UIResponder, UIApplicationDelegate { 12 | var window: UIWindow? 13 | 14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 15 | window = UIWindow(frame: UIScreen.main.bounds) 16 | return true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NiceDemo/SystemFiles/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // NiceDemo 4 | // 5 | // Created by Serhii Kharauzov on 3/21/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let isRunningTests = NSClassFromString("XCTestCase") != nil 12 | let appDelegateClass : AnyClass = isRunningTests ? TestAppDelegate.self : AppDelegate.self 13 | 14 | 15 | UIApplicationMain( 16 | CommandLine.argc, 17 | CommandLine.unsafeArgv, 18 | nil, 19 | NSStringFromClass(appDelegateClass) 20 | ) 21 | -------------------------------------------------------------------------------- /NiceDemo/Utils/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // 4 | // 5 | // Created by Serhii Kharauzov on 1/9/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | // MARK: iOS System Constants 13 | 14 | private enum UIUserInterfaceIdiom: Int { 15 | case unspecified 16 | case phone 17 | case pad 18 | } 19 | 20 | private struct ScreenSize { 21 | static let screenWidth = UIScreen.main.bounds.size.width 22 | static let screenHeight = UIScreen.main.bounds.size.height 23 | static let screenMaxLength = max(ScreenSize.screenWidth, ScreenSize.screenHeight) 24 | static let screenMinLength = min(ScreenSize.screenWidth, ScreenSize.screenHeight) 25 | } 26 | 27 | /// Type of device, based on screen size. 28 | 29 | public struct DeviceType { 30 | public static let isIphone5OrLess = UIDevice.current.userInterfaceIdiom == .phone && ScreenSize.screenMaxLength <= 568.0 31 | public static let isIphone8 = UIDevice.current.userInterfaceIdiom == .phone && ScreenSize.screenMaxLength == 667.0 32 | public static let isIphone8PlusOrMore = UIDevice.current.userInterfaceIdiom == .phone && ScreenSize.screenMaxLength >= 736.0 33 | public static let isIphoneX = UIDevice.current.userInterfaceIdiom == .phone && ScreenSize.screenMaxLength == 812.0 34 | public static let isIpad = UIDevice.current.userInterfaceIdiom == .pad 35 | 36 | public static var hasTopNotch: Bool { 37 | return UIApplication.shared.delegate?.window??.safeAreaInsets.top ?? 0 > 20 38 | } 39 | } 40 | 41 | // MARK: App internal constants 42 | 43 | struct Constants { 44 | static let passwordRequiredLength = 6 45 | } 46 | -------------------------------------------------------------------------------- /NiceDemoTests/AppDelegateService/AppDelegateServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegateServiceTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/24/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: AppDelegateService 14 | /// 15 | 16 | class AppDelegateServiceTests: XCTestCase { 17 | 18 | var appDelegateService: AppDelegateService! 19 | 20 | override func setUp() { 21 | super.setUp() 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | appDelegateService = AppDelegateService(window: UIWindow(frame: .zero)) 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | appDelegateService = nil 29 | super.tearDown() 30 | } 31 | 32 | func testSetupAppCoordinator() { 33 | // when 34 | appDelegateService.setupAppCoordinator() 35 | // then 36 | XCTAssertNotNil(appDelegateService.window.rootViewController, "RootViewController is nil after setup.") 37 | XCTAssertTrue(appDelegateService.window.isKeyWindow, "Window is not key after setup.") 38 | XCTAssertNotNil(appDelegateService.appCoordinator, "AppCoordinator is nil after setup.") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /NiceDemoTests/Coordinators/AppCoordinatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinatorTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/23/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: AppCoordinator 14 | /// 15 | 16 | class AppCoordinatorTests: XCTestCase { 17 | 18 | var coordinator: AppCoordinator! 19 | 20 | override func setUp() { 21 | super.setUp() 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | coordinator = AppCoordinator(navigationController: UINavigationController()) 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | coordinator = nil 29 | super.tearDown() 30 | } 31 | 32 | func testStartMethod() { 33 | // when 34 | coordinator.start() 35 | // then 36 | XCTAssertFalse(coordinator.navigationController.viewControllers.isEmpty, "Coordinator's navigationController must not be empty after `start` method call.") 37 | } 38 | 39 | func testShowDogFlow() { 40 | // when 41 | coordinator.showDogsFlow() 42 | // then 43 | XCTAssertFalse(coordinator.childCoordinators.isEmpty, "Coordinator's childControllers must have value.") 44 | XCTAssertTrue(coordinator.navigationController.visibleViewController is DogsListViewController, "Visible viewcontroller must be of class `DogsListViewController`") 45 | } 46 | 47 | func testShowAuthFlow() { 48 | // when 49 | coordinator.showAuthenticationFlow() 50 | // then 51 | XCTAssertFalse(coordinator.childCoordinators.isEmpty, "Coordinator's childControllers must have value.") 52 | XCTAssertTrue(coordinator.navigationController.visibleViewController is SignInViewController, "Visible viewcontroller must be of class `SignInViewController`") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /NiceDemoTests/Coordinators/AuthFlowCoordinatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthFlowCoordinatorTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/23/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: AuthFlowCoordinator 14 | /// 15 | 16 | class AuthFlowCoordinatorTests: XCTestCase { 17 | 18 | var coordinator: AuthFlowCoordinator! 19 | 20 | override func setUp() { 21 | super.setUp() 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | coordinator = AuthFlowCoordinator(navigationController: UINavigationController()) 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | coordinator = nil 29 | super.tearDown() 30 | } 31 | 32 | func testStartMethod() { 33 | // when 34 | coordinator.start() 35 | // then 36 | XCTAssertFalse(coordinator.navigationController.viewControllers.isEmpty, "Coordinator's navigationController must not be empty after `start` method call.") 37 | } 38 | 39 | func testShowSignInScene() { 40 | // when 41 | coordinator.showSignInScene() 42 | // then 43 | XCTAssertTrue(coordinator.navigationController.visibleViewController is SignInViewController, "Visible viewcontroller must be of class `SignInViewController`") 44 | } 45 | 46 | func testShowForgotPasswordScene() { 47 | // when 48 | coordinator.showForgotPasswordScene() 49 | // then 50 | XCTAssertTrue(coordinator.navigationController.visibleViewController is ForgotPasswordViewController, "Visible viewcontroller must be of class `ForgotPasswordViewController`") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /NiceDemoTests/Coordinators/CoordinatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinatorTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/23/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// Created for testing of Coordinator's protocol default implementation in extension. 13 | class MockCoordinator: Coordinator { 14 | 15 | // MARK: Properties 16 | 17 | var childCoordinators: [Coordinator] = [] 18 | let navigationController: UINavigationController 19 | 20 | // MARK: Public methods 21 | 22 | init(navigationController: UINavigationController = UINavigationController()) { 23 | self.navigationController = navigationController 24 | } 25 | 26 | func start() { 27 | navigationController.setViewControllers([MockViewController()], animated: false) 28 | } 29 | } 30 | 31 | /// Created for testing of Coordinator's protocol default implementation in extension. 32 | class MockViewController: BaseViewController { 33 | } 34 | 35 | /// 36 | /// SUT: MockCoordinator 37 | /// 38 | 39 | class CoordinatorTests: XCTestCase { 40 | 41 | var coordinator: MockCoordinator! 42 | 43 | override func setUp() { 44 | super.setUp() 45 | // Put setup code here. This method is called before the invocation of each test method in the class. 46 | coordinator = MockCoordinator() 47 | } 48 | 49 | override func tearDown() { 50 | // Put teardown code here. This method is called after the invocation of each test method in the class. 51 | coordinator = nil 52 | super.tearDown() 53 | } 54 | 55 | func testStartMethod() { 56 | // when 57 | coordinator.start() 58 | // then 59 | XCTAssertFalse(coordinator.navigationController.viewControllers.isEmpty, "Coordinator's navigationController must not be empty after `start` method call.") 60 | } 61 | 62 | func testChildCoordinatorsCollection() { 63 | let testCoordinator = MockCoordinator() 64 | // when 65 | coordinator.addChildCoordinator(testCoordinator) 66 | // then 67 | XCTAssertNotNil(coordinator.childCoordinators.filter({$0 === testCoordinator}).first, "Added `testCoordiantor` must exists at childCoordinators collection.") 68 | // when 69 | coordinator.addChildCoordinator(testCoordinator) 70 | // then 71 | XCTAssertEqual(coordinator.childCoordinators.count, 1, "Added `testCoordiantor` must still exists in single form.") 72 | // when 73 | coordinator.removeChildCoordinator(testCoordinator) 74 | // then 75 | XCTAssertNil(coordinator.childCoordinators.filter({$0 === testCoordinator}).first, "Removed `testCoordiantor` must not exists at childCoordinators collection.") 76 | } 77 | 78 | func testPop() { 79 | // when 80 | // we added `MockViewController` as second item in collection due to possibility 81 | // performing `popViewController` under it at UINavigationController 82 | let mockViewController = MockViewController() 83 | coordinator.navigationController.setViewControllers([UIViewController(), mockViewController], animated: false) 84 | // then 85 | XCTAssertNotNil(coordinator.navigationController.viewControllers.filter({$0 === mockViewController}).first, "`MockViewController` must exists at navigationController.viewControllers collection after `start` call.") 86 | // when 87 | coordinator.popViewController(animated: false) 88 | // then 89 | XCTAssertNil(coordinator.navigationController.viewControllers.filter({$0 === mockViewController}).first, "`MockViewController` must not exists at navigationController.viewControllers collection after `pop` call.") 90 | } 91 | 92 | func testDismiss() { 93 | // when 94 | UIApplication.shared.delegate?.window??.rootViewController = coordinator.navigationController 95 | UIApplication.shared.delegate?.window??.makeKeyAndVisible() 96 | coordinator.navigationController.setViewControllers([UIViewController()], animated: false) 97 | let mockViewController = MockViewController() 98 | coordinator.navigationController.present(mockViewController, animated: false, completion: nil) 99 | // then 100 | XCTAssertTrue(coordinator.navigationController.presentedViewController === mockViewController, "PresentedViewController must be `MockViewController`") 101 | // when 102 | let promise = expectation(description: "Completion handler invoked") 103 | coordinator.dismissViewController(animated: false) { 104 | promise.fulfill() 105 | } 106 | waitForExpectations(timeout: 2, handler: nil) 107 | // then 108 | XCTAssertNil(coordinator.navigationController.presentedViewController, "PresentedViewController must be nil") 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /NiceDemoTests/Coordinators/DogsFlowCoordinatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsFlowCoordinatorTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/23/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: DogsFlowCoordinator 14 | /// 15 | 16 | class DogsFlowCoordinatorTests: XCTestCase { 17 | 18 | var coordinator: DogsFlowCoordinator! 19 | 20 | override func setUp() { 21 | super.setUp() 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | coordinator = DogsFlowCoordinator(navigationController: UINavigationController()) 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | coordinator = nil 29 | super.tearDown() 30 | } 31 | 32 | func testStartMethod() { 33 | // when 34 | coordinator.start() 35 | // then 36 | XCTAssertFalse(coordinator.navigationController.viewControllers.isEmpty, "Coordinator's navigationController must not be empty after `start` method call.") 37 | } 38 | 39 | func testShowDogsListScene() { 40 | // when 41 | coordinator.showDogsListScene() 42 | // then 43 | XCTAssertTrue(coordinator.navigationController.visibleViewController is DogsListViewController, "Visible viewcontroller must be of class `DogsListViewController`") 44 | } 45 | 46 | func testShowDogGalleryScene() { 47 | // when 48 | let dog = Dog(breed: "", subbreeds: []) 49 | coordinator.showDogGalleryScene(dog: dog) 50 | // then 51 | XCTAssertTrue(coordinator.navigationController.visibleViewController is DogGalleryViewController, "Visible viewcontroller must be of class `DogGalleryViewController`") 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /NiceDemoTests/CustomViews/HUDViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HUDViewTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/24/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: HUDView 14 | /// 15 | 16 | class HUDViewTests: XCTestCase { 17 | 18 | var hud: HUDView! 19 | 20 | override func setUp() { 21 | super.setUp() 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | hud = HUDView(frame: CGRect.zero, backgroundColor: nil, tintColor: nil) 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | hud = nil 29 | super.tearDown() 30 | } 31 | 32 | func testHUDAnimating() { 33 | XCTAssertFalse(hud.isAnimating(), "HUD must be idle at this moment.") 34 | // when 35 | hud.startAnimating() 36 | // then 37 | XCTAssertTrue(hud.isAnimating(), "HUD must be active at this moment") 38 | // when 39 | hud.stopAnimating() 40 | // then 41 | XCTAssertFalse(hud.isAnimating(), "HUD must be idle at this moment.") 42 | } 43 | 44 | func testHUDViewAsSubview() { 45 | let view = UIView(frame: .zero) 46 | // when 47 | view.showHUD(animated: true) 48 | // then 49 | var oldHUD = HUDView.hudIn(view: view) 50 | XCTAssertNotNil(oldHUD, "HUD must be subview of view.") 51 | // when 52 | view.hideHUD(animated: false) 53 | // then 54 | oldHUD = HUDView.hudIn(view: view) 55 | XCTAssertNil(oldHUD, "HUD must be removed from superview and be nil.") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /NiceDemoTests/CustomViews/LoadingTableViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingTableViewTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 1/20/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: LoadingTableViewProvider 14 | /// 15 | 16 | class LoadingTableViewTests: XCTestCase { 17 | 18 | var tableView: UITableView! 19 | var tableViewProvider: LoadingTableViewProvider! 20 | let definedNumberOfSections = 2 21 | let definedNumberOfRowsPerSection = 9 22 | 23 | override func setUp() { 24 | // Put setup code here. This method is called before the invocation of each test method in the class. 25 | tableView = UITableView() 26 | tableView.register(LoadingTableViewCell.self, forCellReuseIdentifier: LoadingTableViewCell.reuseIdentifier) 27 | let configuration = LoadingTableViewProvider.Configuration(numberOfSections: definedNumberOfSections, numberOfRowsInSection: definedNumberOfRowsPerSection) 28 | tableViewProvider = LoadingTableViewProvider(configuration: configuration) 29 | tableView.dataSource = tableViewProvider 30 | } 31 | 32 | override func tearDown() { 33 | // Put teardown code here. This method is called after the invocation of each test method in the class. 34 | tableViewProvider = nil 35 | tableView = nil 36 | } 37 | 38 | func testTableViewDataSource() { 39 | // when 40 | XCTAssertNotNil(tableView.dataSource) 41 | 42 | // then 43 | XCTAssertEqual(tableView.dataSource?.numberOfSections?(in: tableView), definedNumberOfSections, "Number of sections must be equal to `definedNumberOfSections`.") 44 | XCTAssertEqual(tableView.dataSource?.tableView(tableView, numberOfRowsInSection: 0), definedNumberOfRowsPerSection, "Number of rows must be equal to `definedNumberOfRowsPerSection`.") 45 | } 46 | 47 | func testTableViewCell() { 48 | // when 49 | let cell = tableView.dataSource?.tableView(tableView, cellForRowAt: IndexPath(row: 0, section: 0)) as? LoadingTableViewCell 50 | 51 | // then 52 | XCTAssertNotNil(cell, "Must exists.") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /NiceDemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NiceDemoTests/Network/ImageLoaderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageLoaderTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/23/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: SimpleImageLoader 14 | /// 15 | 16 | class ImageLoaderTests: XCTestCase { 17 | 18 | var imageLoader: SimpleImageLoader! 19 | var imageLoaderCore: MockImageLoaderCore! 20 | 21 | override func setUp() { 22 | super.setUp() 23 | imageLoaderCore = MockImageLoaderCore() 24 | imageLoader = SimpleImageLoader(core: imageLoaderCore) 25 | } 26 | 27 | override func tearDown() { 28 | imageLoader = nil 29 | imageLoaderCore = nil 30 | super.tearDown() 31 | } 32 | 33 | func testLoadingOfImageByValidUrl() { 34 | // given 35 | var responseData: Any? 36 | let validUrlString = "https://mock.com/valid.jpg" 37 | // when 38 | imageLoader.loadImageFrom(urlString: validUrlString) { (image) in 39 | responseData = image 40 | } 41 | // then 42 | XCTAssertNotNil(responseData) 43 | } 44 | 45 | func testLoadingOfImageByInvalidUrl() { 46 | // given 47 | var responseData: Any? 48 | let notValidUrlString = "" 49 | // when 50 | imageLoader.loadImageFrom(urlString: notValidUrlString) { (image) in 51 | responseData = image 52 | } 53 | // then 54 | XCTAssertNil(responseData) 55 | } 56 | } 57 | 58 | class MockImageLoaderCore: ImageLoaderCore { 59 | func getImageData(from url: URL, completion: @escaping (_ data: Data?) -> Void) { 60 | completion(#imageLiteral(resourceName: "dog").pngData()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /NiceDemoTests/Network/NetworkTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/21/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: DogsServerService 14 | /// 15 | 16 | class NiceDemoNetworkTests: XCTestCase { 17 | 18 | var dogsServerService: DogsServerService! 19 | var dogsDataParser: MockDogsApiDataParser! 20 | var core: MockServerService! 21 | 22 | override func setUp() { 23 | super.setUp() 24 | dogsDataParser = MockDogsApiDataParser() 25 | core = MockServerService() 26 | dogsServerService = DogsServerService(core: core, parser: dogsDataParser) 27 | } 28 | 29 | override func tearDown() { 30 | dogsServerService = nil 31 | super.tearDown() 32 | } 33 | 34 | func testGetAllDogsValidNetworkRequest() { 35 | // given 36 | var responseData: Any? 37 | var responseError: Error? 38 | let getAllDogsUrlPath = ServerRouter.getAllDogsBreeds.asURLRequest().url?.absoluteString 39 | // when 40 | dogsServerService.getAllDogs { (dogs, error) in 41 | responseData = dogs 42 | responseError = error 43 | } 44 | // then 45 | XCTAssertNil(responseError) 46 | XCTAssertNotNil(responseData) 47 | XCTAssertEqual(getAllDogsUrlPath, core.requestUrlString) 48 | } 49 | 50 | func testGetAllDogsNotValidNetworkRequest() { 51 | // given 52 | var responseData: Any? 53 | var responseError: Error? 54 | let getAllDogsUrlPath = ServerRouter.getAllDogsBreeds.asURLRequest().url?.absoluteString 55 | core.shouldFailRequest = true 56 | // when 57 | dogsServerService.getAllDogs { (dogs, error) in 58 | responseData = dogs 59 | responseError = error 60 | } 61 | // then 62 | XCTAssertNotNil(responseError) 63 | XCTAssertNil(responseData) 64 | XCTAssertEqual(getAllDogsUrlPath, core.requestUrlString) 65 | } 66 | 67 | func testGetDogRandomImageValidUrlRequest() { 68 | // given 69 | var responseData: Any? 70 | var responseError: Error? 71 | let breed = "akita" 72 | let getDogRandomImageUrlPath = ServerRouter.getDogRandomImage(breed).asURLRequest().url?.absoluteString 73 | // when 74 | dogsServerService.getDogRandomImageUrl(breed: breed) { (urlString, error) in 75 | responseData = urlString 76 | responseError = error 77 | } 78 | // then 79 | XCTAssertNil(responseError) 80 | XCTAssertNotNil(responseData) 81 | XCTAssertEqual(getDogRandomImageUrlPath, core.requestUrlString) 82 | } 83 | 84 | func testGetDogRandomImageNotValidUrlRequest() { 85 | // given 86 | var responseData: Any? 87 | var responseError: Error? 88 | let breed = "akita" 89 | let getDogRandomImageUrlPath = ServerRouter.getDogRandomImage(breed).asURLRequest().url?.absoluteString 90 | core.shouldFailRequest = true 91 | // when 92 | dogsServerService.getDogRandomImageUrl(breed: breed) { (urlString, error) in 93 | responseData = urlString 94 | responseError = error 95 | } 96 | // then 97 | XCTAssertNotNil(responseError) 98 | XCTAssertNil(responseData) 99 | XCTAssertEqual(getDogRandomImageUrlPath, core.requestUrlString) 100 | } 101 | } 102 | 103 | class MockServerService: ServerService { 104 | var requestUrlString: String? 105 | var shouldFailRequest = false 106 | 107 | func performRequest(_ request: URLRequestable, completion: ServerRequestResponseCompletion?) { 108 | requestUrlString = request.asURLRequest().url?.absoluteString 109 | if shouldFailRequest { 110 | completion?(Result.failure(NSError(domain: "", code: 0, userInfo: nil))) 111 | } else if let data = Data() as? T { 112 | completion?(Result.success(data)) 113 | } 114 | } 115 | } 116 | 117 | class MockDogsApiDataParser: DogsServerResponseParser { 118 | func parseGetAllDogsResponse(data: Data) -> [Dog]? { 119 | return [Dog(breed: "Test", subbreeds: nil)] 120 | } 121 | 122 | func parseGetDogRandomImageUrlResponse(data: Data) -> String? { 123 | return "image.jpg" 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /NiceDemoTests/Other/ReusableViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReusableViewTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/24/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | 13 | class MockView: ReusableView { 14 | } 15 | 16 | /// 17 | /// SUT: ReusableView 18 | /// 19 | 20 | class ReusableViewTests: XCTestCase { 21 | func testReuseIdentifier() { 22 | XCTAssertEqual(MockView.reuseIdentifier, "MockView", "ReuseIndentifier must match class name.") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/DogGallery/DogGalleryCollectionViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogGalleryCollectionViewTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 18.04.2020. 6 | // Copyright © 2020 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: DogGalleryCollectionView 14 | /// 15 | 16 | class DogGalleryCollectionViewTests: XCTestCase { 17 | 18 | var collectionViewProvider: DogBreedsCollectionViewProvider! 19 | var collectionView: UICollectionView! 20 | let data = ["Test1", "Test2", "Test3"] 21 | 22 | override func setUp() { 23 | collectionViewProvider = DogBreedsCollectionViewProvider() 24 | collectionViewProvider.data = data 25 | collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout()) 26 | collectionView.register(DogBreedCollectionViewCell.self, forCellWithReuseIdentifier: DogBreedCollectionViewCell.reuseIdentifier) 27 | collectionView.delegate = collectionViewProvider 28 | collectionView.dataSource = collectionViewProvider 29 | } 30 | 31 | override func tearDown() { 32 | collectionView = nil 33 | collectionViewProvider = nil 34 | } 35 | 36 | func testNumberOfItems() { 37 | // then 38 | XCTAssertEqual(collectionViewProvider.collectionView(collectionView, numberOfItemsInSection: 0), data.count) 39 | } 40 | 41 | func testCellForItem() { 42 | // given 43 | guard let cell = collectionViewProvider.collectionView(collectionView, cellForItemAt: IndexPath(row: 0, section: 0)) as? DogBreedCollectionViewCell else { 44 | XCTFail("Cell must exist") 45 | return 46 | } 47 | // then 48 | XCTAssertEqual(cell.titleLabel.text, data[0]) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/DogGallery/DogGalleryConfiguratorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogGalleryConfiguratorTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 1/20/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: DogGalleryConfigurator 14 | /// 15 | 16 | class DogGalleryConfiguratorTests: XCTestCase { 17 | 18 | var configurator: DogGalleryConfigurator! 19 | 20 | func testConfiguratorBuildResult() { 21 | // when 22 | configurator = DogGalleryConfigurator() 23 | let dog = Dog(breed: "", subbreeds: []) 24 | let configuredViewController = configurator.configuredViewController(dog: dog, delegate: nil) 25 | // then 26 | XCTAssertNotNil(configuredViewController.presenter, "Presenter should be coupled to viewController") 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/DogGallery/DogGalleryPresenterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogGalleryPresenterTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 1/24/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: DogGalleryPresenter 14 | /// 15 | 16 | class DogGalleryPresenterTests: XCTestCase { 17 | 18 | var viewController: DogGalleryViewControllerMock! 19 | var presenter: DogGalleryPresenter! 20 | var serverService: DogGalleryServerServiceMock! 21 | var storageService: DogGalleryStorageServiceMock! 22 | var imageLoader: ImageLoaderMock! 23 | 24 | override func setUp() { 25 | // Put setup code here. This method is called before the invocation of each test method in the class. 26 | viewController = DogGalleryViewControllerMock() 27 | serverService = DogGalleryServerServiceMock() 28 | storageService = DogGalleryStorageServiceMock() 29 | imageLoader = ImageLoaderMock() 30 | presenter = DogGalleryPresenter(view: viewController, dog: Dog(breed: "", subbreeds: []), serverService: serverService, storageService: storageService, imageLoader: imageLoader) 31 | } 32 | 33 | override func tearDown() { 34 | // Put teardown code here. This method is called after the invocation of each test method in the class. 35 | viewController = nil 36 | presenter = nil 37 | } 38 | 39 | func testLoadingRandomImageViewState() { 40 | // given 41 | serverService.testCompletionResult = "some real url" 42 | // when 43 | XCTAssertEqual(imageLoader.loadImageFromDidCalled, 0) 44 | XCTAssertEqual(serverService.getDogRandomImageUrlDidCalled, 0) 45 | presenter.loadRandomDogImage() 46 | // then 47 | XCTAssertEqual(imageLoader.loadImageFromDidCalled, 1) 48 | XCTAssertEqual(serverService.getDogRandomImageUrlDidCalled, 1) 49 | } 50 | 51 | func testResultRandomImageViewState() { 52 | // when 53 | viewController.showHUD(animated: true) 54 | XCTAssertEqual(viewController.isDogImageSet, false) 55 | XCTAssertEqual(viewController.isHudDisplayed, true) 56 | let resultImgae = #imageLiteral(resourceName: "pawPrintSelected") 57 | presenter.updateViewBasedOn(state: .resultRandomImage(resultImgae)) 58 | // then 59 | XCTAssertEqual(viewController.isDogImageSet, true) 60 | XCTAssertEqual(viewController.isHudDisplayed, false) 61 | } 62 | 63 | func testErrorGettingRandomImage() { 64 | // when 65 | viewController.showHUD(animated: true) 66 | XCTAssertEqual(viewController.isHudDisplayed, true) 67 | presenter.updateViewBasedOn(state: .errorGettingRandomImage(message: "")) 68 | // then 69 | XCTAssertEqual(viewController.isHudDisplayed, false) 70 | } 71 | 72 | func testHandleFavouriteButtonTap() { 73 | // given 74 | storageService.favouriteDogBreedStub = nil 75 | 76 | // when 77 | presenter.handleFavouriteButtonTap() 78 | // then 79 | XCTAssertEqual(viewController.isRightBarButtonHighlighted, true) 80 | 81 | // when 82 | presenter.handleFavouriteButtonTap() 83 | // then 84 | XCTAssertEqual(viewController.isRightBarButtonHighlighted, false) 85 | } 86 | 87 | func testShowDogSubbreeds() { 88 | // given 89 | let testSubbreeds = ["Breed #1", "Breed #2"] 90 | // when 91 | presenter.showDogSubbreeds(testSubbreeds) 92 | // then 93 | XCTAssertEqual(presenter.collectionViewProvider.data.count, testSubbreeds.count) 94 | } 95 | 96 | func testUpdateDataLabelVisibility() { 97 | // when 98 | presenter.updateDataLabelVisibility(hasSubbreeds: true) 99 | // then 100 | XCTAssertEqual(viewController.isNoDataLabelHidden, true) 101 | 102 | // when 103 | presenter.updateDataLabelVisibility(hasSubbreeds: false) 104 | // then 105 | XCTAssertEqual(viewController.isNoDataLabelHidden, false) 106 | } 107 | } 108 | 109 | class ImageLoaderMock: ImageLoader { 110 | var loadImageFromDidCalled = 0 111 | var testCompletionResult: UIImage? 112 | 113 | func loadImageFrom(urlString: String, completion: @escaping (UIImage?) -> Void) { 114 | loadImageFromDidCalled += 1 115 | completion(testCompletionResult) 116 | } 117 | } 118 | 119 | class DogGalleryServerServiceMock: DogGalleryServerProtocol { 120 | var getDogRandomImageUrlDidCalled = 0 121 | var testCompletionResult: String? 122 | var testCompletionError: Error? 123 | 124 | func getDogRandomImageUrl(breed: String, completion: @escaping (String?, Error?) -> Void) { 125 | getDogRandomImageUrlDidCalled += 1 126 | completion(testCompletionResult, testCompletionError) 127 | } 128 | } 129 | 130 | class DogGalleryStorageServiceMock: DogGalleryStorageProtocol { 131 | var favouriteDogBreedStub: String? 132 | 133 | func getFavouriteDogBreed() -> String? { 134 | return favouriteDogBreedStub 135 | } 136 | 137 | func setFavouriteDogBreed(_ breed: String?) { 138 | favouriteDogBreedStub = breed 139 | } 140 | } 141 | 142 | class DogGalleryViewControllerMock: DogGalleryViewInterface { 143 | var isHudDisplayed = false 144 | var isDogImageSet = false 145 | var isRightBarButtonHighlighted = false 146 | var isNoDataLabelHidden = false 147 | 148 | func setDogImage(_ image: UIImage, animated: Bool) { 149 | isDogImageSet = true 150 | } 151 | 152 | func setNavigationTitle(_ title: String) { 153 | 154 | } 155 | 156 | func showHUD(animated: Bool) { 157 | isHudDisplayed = true 158 | } 159 | 160 | func hideHUD(animated: Bool) { 161 | isHudDisplayed = false 162 | } 163 | 164 | func setRightBarButtonItemHighlightState(_ isOn: Bool, animated: Bool) { 165 | isRightBarButtonHighlighted = isOn 166 | } 167 | 168 | func setCollectionViewProvider(_ provider: CollectionViewProvider) { 169 | 170 | } 171 | 172 | func reloadCollectionView() { 173 | 174 | } 175 | 176 | func showNoDataLabel() { 177 | isNoDataLabelHidden = false 178 | } 179 | 180 | func hideNoDataLabel() { 181 | isNoDataLabelHidden = true 182 | } 183 | } 184 | 185 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/DogGallery/DogGalleryViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogGalleryViewControllerTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 18.04.2020. 6 | // Copyright © 2020 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: DogGalleryViewController 14 | /// 15 | 16 | class DogGalleryViewControllerTests: XCTestCase { 17 | 18 | var viewController: DogGalleryViewController! 19 | var presenter: MockDogGalleryPresenter! 20 | 21 | override func setUp() { 22 | presenter = MockDogGalleryPresenter() 23 | viewController = DogGalleryViewController() 24 | viewController.setPresenter(presenter) 25 | _ = viewController.view 26 | } 27 | 28 | override func tearDown() { 29 | viewController = nil 30 | } 31 | 32 | func testSetPresenter() { 33 | let testPresenter = MockDogGalleryPresenter() 34 | viewController.setPresenter(testPresenter) 35 | XCTAssertTrue(testPresenter === viewController.presenter) 36 | } 37 | 38 | func testViewDidLoad() { 39 | // assuming, that viewDidLoad() was called 40 | // due to line `_ = viewController.view` 41 | 42 | // when 43 | viewController.customView.didPressActionButton?() 44 | 45 | // then 46 | XCTAssertNotNil(viewController.customView.didPressActionButton) 47 | XCTAssertNotNil(viewController.navigationItem.rightBarButtonItem) 48 | XCTAssertEqual(presenter.onViewDidLoadCounter, 1) 49 | XCTAssertEqual(presenter.handleActionButtonTapCounter, 1) 50 | } 51 | 52 | func testFavouriteButtonTap() { 53 | // given 54 | XCTAssertEqual(presenter.handleFavouriteButtonTapCounter, 0) 55 | // when 56 | viewController.favouriteButtonTapped() 57 | // then 58 | XCTAssertEqual(presenter.handleFavouriteButtonTapCounter, 1) 59 | } 60 | 61 | func testSetCollectionViewProvider() { 62 | // given 63 | XCTAssertNil(viewController.customView.collectionView.dataSource) 64 | XCTAssertNil(viewController.customView.collectionView.delegate) 65 | // when 66 | let collectionViewProvider = DogBreedsCollectionViewProvider() 67 | viewController.setCollectionViewProvider(collectionViewProvider) 68 | // then 69 | XCTAssertNotNil(viewController.customView.collectionView.dataSource) 70 | XCTAssertNotNil(viewController.customView.collectionView.delegate) 71 | } 72 | 73 | func testSetDogImage() { 74 | // given 75 | XCTAssertNil(viewController.customView.dogImageView.image) 76 | let image = #imageLiteral(resourceName: "dog") 77 | // when 78 | viewController.setDogImage(image, animated: false) 79 | // then 80 | XCTAssertEqual(image, viewController.customView.dogImageView.image) 81 | } 82 | 83 | func testSetNavigationTitle() { 84 | // given 85 | XCTAssertNil(viewController.navigationItem.title) 86 | let title = "test" 87 | // when 88 | viewController.setNavigationTitle(title) 89 | // then 90 | XCTAssertEqual(title, viewController.navigationItem.title) 91 | } 92 | 93 | func testShowHUD() { 94 | // given 95 | XCTAssertNil(HUDView.hudIn(view: viewController.customView.containerView)) 96 | // when 97 | viewController.showHUD(animated: false) 98 | // then 99 | XCTAssertNotNil(HUDView.hudIn(view: viewController.customView.containerView)) 100 | } 101 | 102 | func testHideHUD() { 103 | // given 104 | viewController.showHUD(animated: false) 105 | XCTAssertNotNil(HUDView.hudIn(view: viewController.customView.containerView)) 106 | // when 107 | viewController.hideHUD(animated: false) 108 | // then 109 | XCTAssertNil(HUDView.hudIn(view: viewController.customView.containerView)) 110 | } 111 | 112 | func testSetRightBarButtonItemHighlightState() { 113 | // when 114 | viewController.setRightBarButtonItemHighlightState(true, animated: false) 115 | // then 116 | XCTAssertEqual(viewController.navigationItem.rightBarButtonItem?.image, #imageLiteral(resourceName: "pawPrintSelected").withRenderingMode(.alwaysTemplate)) 117 | 118 | // when 119 | viewController.setRightBarButtonItemHighlightState(false, animated: false) 120 | // then 121 | XCTAssertEqual(viewController.navigationItem.rightBarButtonItem?.image, #imageLiteral(resourceName: "pawPrintNotSelected").withRenderingMode(.alwaysTemplate)) 122 | 123 | // when 124 | viewController.navigationItem.rightBarButtonItem = nil 125 | viewController.setRightBarButtonItemHighlightState(false, animated: false) 126 | // then 127 | XCTAssertNil(viewController.navigationItem.rightBarButtonItem?.image) 128 | } 129 | 130 | func testShowNoDataLabel() { 131 | // given 132 | viewController.hideNoDataLabel() 133 | XCTAssertTrue(viewController.customView.noDataLabel.isHidden) 134 | // when 135 | viewController.showNoDataLabel() 136 | // then 137 | XCTAssertFalse(viewController.customView.noDataLabel.isHidden) 138 | } 139 | } 140 | 141 | class MockDogGalleryPresenter: DogGalleryPresentation { 142 | var onViewDidLoadCounter = 0 143 | var handleActionButtonTapCounter = 0 144 | var handleFavouriteButtonTapCounter = 0 145 | 146 | func onViewDidLoad() { 147 | onViewDidLoadCounter += 1 148 | } 149 | 150 | func handleActionButtonTap() { 151 | handleActionButtonTapCounter += 1 152 | } 153 | 154 | func handleFavouriteButtonTap() { 155 | handleFavouriteButtonTapCounter += 1 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/DogsList/DogsListConfiguratorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsListConfiguratorTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 1/20/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: DogsListConfigurator 14 | /// 15 | 16 | class DogsListConfiguratorTests: XCTestCase { 17 | 18 | var configurator: DogsListConfigurator! 19 | 20 | func testConfiguratorBuildResult() { 21 | // when 22 | configurator = DogsListConfigurator() 23 | let configuredViewController = configurator.configuredViewController(delegate: nil) 24 | // then 25 | XCTAssertNotNil(configuredViewController.presenter, "Presenter should be coupled to viewController") 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/DogsList/DogsListSceneTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsListSceneTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 1/20/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: DogsListViewController, DogsListPresenter 14 | /// 15 | 16 | class DogsListSceneTests: XCTestCase { 17 | 18 | var viewController: DogsListViewController! 19 | var presenter: DogsListPresenter! 20 | 21 | override func setUp() { 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | viewController = DogsListViewController() 24 | presenter = DogsListPresenter(view: viewController) 25 | viewController.setPresenter(presenter) 26 | } 27 | 28 | override func tearDown() { 29 | // Put teardown code here. This method is called after the invocation of each test method in the class. 30 | viewController = nil 31 | presenter = nil 32 | } 33 | 34 | func testShowOfFavouriteButton() { 35 | // when 36 | presenter.updateFavouriteButtonVisibility(hasFavouriteDog: true) 37 | // then 38 | XCTAssertNotNil(viewController.navigationItem.rightBarButtonItem, "Right barButtonItem must exist") 39 | } 40 | 41 | func testHideOfFavouriteButton() { 42 | // when 43 | presenter.updateFavouriteButtonVisibility(hasFavouriteDog: true) 44 | presenter.updateFavouriteButtonVisibility(hasFavouriteDog: false) 45 | // then 46 | XCTAssertNil(viewController.navigationItem.rightBarButtonItem, "Right barButtonItem must not exist") 47 | } 48 | 49 | func testLoadingViewState() { 50 | // when 51 | presenter.updateViewBasedOn(state: .loading) 52 | let setTableViewDataSource = viewController.customView.tableView.dataSource as? LoadingTableViewProvider 53 | let setTableViewDelegate = viewController.customView.tableView.delegate as? LoadingTableViewProvider 54 | // then 55 | XCTAssertEqual(setTableViewDataSource, presenter.loadingTableViewProvider) 56 | XCTAssertEqual(setTableViewDelegate, presenter.loadingTableViewProvider) 57 | } 58 | 59 | func testResultViewState() { 60 | // when 61 | XCTAssertTrue(presenter.fetchedData.isEmpty, "`fetchedData` array must be empty before result state occured.") 62 | let data = [Dog(breed: "Dog1", subbreeds: []), Dog(breed: "Dog2", subbreeds: [])] 63 | presenter.updateViewBasedOn(state: .result(data)) 64 | let setTableViewDataSource = viewController.customView.tableView.dataSource as? DogsListTableViewProvider 65 | let setTableViewDelegate = viewController.customView.tableView.delegate as? DogsListTableViewProvider 66 | // then 67 | XCTAssertEqual(setTableViewDataSource, presenter.contentTableViewProvider) 68 | XCTAssertEqual(setTableViewDelegate, presenter.contentTableViewProvider) 69 | XCTAssertTrue(!presenter.fetchedData.isEmpty, "`fetchedData` array must be not empty when existing data was fetched from server.") 70 | } 71 | 72 | func testErrorViewState() { 73 | //when 74 | let testRect = CGRect(x: 0, y: 0, width: 400, height: 600) 75 | let window = UIWindow(frame: testRect) 76 | window.rootViewController = viewController 77 | window.makeKeyAndVisible() 78 | XCTAssertNil(viewController.presentedViewController) 79 | presenter.updateViewBasedOn(state: .error(message: "")) 80 | //then 81 | XCTAssertTrue(viewController.presentedViewController is UIAlertController) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/DogsList/DogsListTableViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogsListTableViewTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/24/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: DogsListTableView Components: DogsListTableViewProvider, DogsListTableViewProviderDelegate, 14 | /// DogBreedTableViewCell 15 | /// 16 | 17 | class DogsListTableViewTests: XCTestCase { 18 | 19 | var tableView: UITableView! 20 | var tableViewProvider: DogsListTableViewProvider! 21 | var selectedIndex: Int? 22 | 23 | override func setUp() { 24 | super.setUp() 25 | // Put setup code here. This method is called before the invocation of each test method in the class. 26 | tableView = UITableView() 27 | tableView.register(DogBreedTableViewCell.self, forCellReuseIdentifier: DogBreedTableViewCell.reuseIdentifier) 28 | tableViewProvider = DogsListTableViewProvider() 29 | tableViewProvider.didSelectItem = { [unowned self] (_ atIndex: Int) in 30 | self.selectedIndex = atIndex 31 | } 32 | let data = [DogBreedViewModel(title: "Akita", subtitle: "Westhighland, Yorkshire"), DogBreedViewModel(title: "Terrier", subtitle: "")] 33 | tableViewProvider.data = data 34 | tableView.dataSource = tableViewProvider 35 | tableView.delegate = tableViewProvider 36 | } 37 | 38 | override func tearDown() { 39 | // Put teardown code here. This method is called after the invocation of each test method in the class. 40 | tableViewProvider = nil 41 | tableView = nil 42 | selectedIndex = nil 43 | super.tearDown() 44 | } 45 | 46 | func testTableViewDataSource() { 47 | // then 48 | XCTAssertEqual(tableViewProvider.tableView(tableView, numberOfRowsInSection: 0), 2, "Number of rows must be equal number of items in `data` property.") 49 | } 50 | 51 | func testTableViewCell() { 52 | // when 53 | let cell = tableViewProvider.tableView(tableView, cellForRowAt: IndexPath(row: 0, section: 0)) as? DogBreedTableViewCell 54 | let title = cell?.getTitle() 55 | let subtitle = cell?.getSubtitle() 56 | 57 | // then 58 | XCTAssertNotNil(cell, "Must exists.") 59 | XCTAssertEqual(title, "Akita", "Dismatch.") 60 | XCTAssertEqual(subtitle, "Westhighland, Yorkshire", "Dismatch.") 61 | } 62 | 63 | func testTableViewDelegate() { 64 | // when 65 | tableViewProvider.tableView(tableView, didSelectRowAt: IndexPath(row: 0, section: 0)) 66 | // then 67 | XCTAssertEqual(selectedIndex, 0, "Index must be equal to 0.") 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/ForgotPassword/ForgotPasswordConfiguratorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordConfiguratorTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 1/24/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: ForgotPasswordConfigurator 14 | /// 15 | 16 | class ForgotPasswordConfiguratorTests: XCTestCase { 17 | 18 | var configurator: ForgotPasswordConfigurator! 19 | 20 | func testConfiguratorBuildResult() { 21 | // when 22 | configurator = ForgotPasswordConfigurator() 23 | let configuredViewController = configurator.configuredViewController(delegate: nil) 24 | // then 25 | XCTAssertNotNil(configuredViewController.presenter, "Presenter should be coupled to viewController") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/ForgotPassword/ForgotPasswordPresenterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordPresenterTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 1/24/19. 6 | // Copyright © 2019 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: ForgotPasswordPresenter 14 | /// 15 | 16 | class ForgotPasswordPresenterTests: XCTestCase { 17 | 18 | var presenter: ForgotPasswordPresenter! 19 | var viewController: ForgotPasswordViewControllerMock! 20 | var delegate: ForgotPasswordSceneDelegateMock! 21 | 22 | override func setUp() { 23 | // Put setup code here. This method is called before the invocation of each test method in the class. 24 | viewController = ForgotPasswordViewControllerMock() 25 | presenter = ForgotPasswordPresenter(view: viewController) 26 | delegate = ForgotPasswordSceneDelegateMock() 27 | } 28 | 29 | override func tearDown() { 30 | // Put teardown code here. This method is called after the invocation of each test method in the class. 31 | viewController = nil 32 | presenter = nil 33 | delegate = nil 34 | } 35 | 36 | func testHandleSubmitButtonTapWithCorrectEmail() { 37 | // given 38 | viewController.emailStub = "test@email.com" 39 | presenter.delegate = delegate 40 | // when 41 | presenter.handleSubmitButtonTap() 42 | // then 43 | XCTAssertEqual(delegate.isUserPerformedPasswordRecovery, true) 44 | } 45 | 46 | func testHandleSubmitButtonTapWithIncorrectEmail() { 47 | // given 48 | viewController.emailStub = "test_email.com" 49 | presenter.delegate = delegate 50 | // when 51 | presenter.handleSubmitButtonTap() 52 | // then 53 | XCTAssertEqual(delegate.isUserPerformedPasswordRecovery, false) 54 | } 55 | 56 | func testKeyboardAppearanceOnSubmitButtonTap() { 57 | // given 58 | viewController.isKeyboardPresented = true 59 | // when 60 | presenter.handleSubmitButtonTap() 61 | // then 62 | XCTAssertEqual(viewController.isKeyboardPresented, false) 63 | } 64 | } 65 | 66 | class ForgotPasswordViewControllerMock: ForgotPasswordViewInterface { 67 | var emailStub: String? 68 | var isKeyboardPresented = false 69 | 70 | func hideKeyboard() { 71 | isKeyboardPresented = false 72 | } 73 | 74 | func getEmailText() -> String? { 75 | return emailStub 76 | } 77 | 78 | func showAlert(title: String?, message: String?, completion: (() -> Void)?) { 79 | completion?() 80 | } 81 | } 82 | 83 | class ForgotPasswordSceneDelegateMock: ForgotPasswordSceneDelegate { 84 | var isUserPerformedPasswordRecovery = false 85 | 86 | func userPerformedPasswordRecovery() { 87 | isUserPerformedPasswordRecovery = true 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/ForgotPassword/ForgotPasswordViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordViewControllerTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 18.04.2020. 6 | // Copyright © 2020 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: ForgotPasswordViewController 14 | /// 15 | 16 | class ForgotPasswordViewControllerTests: XCTestCase { 17 | 18 | var viewController: ForgotPasswordViewController! 19 | var presenter: MockForgotPasswordPresenter! 20 | 21 | override func setUp() { 22 | presenter = MockForgotPasswordPresenter() 23 | viewController = ForgotPasswordViewController() 24 | viewController.presenter = presenter 25 | _ = viewController.view 26 | } 27 | 28 | override func tearDown() { 29 | viewController = nil 30 | } 31 | 32 | func testSetPresenter() { 33 | let testPresenter = MockForgotPasswordPresenter() 34 | viewController.setPresenter(testPresenter) 35 | XCTAssertTrue(testPresenter === viewController.presenter) 36 | } 37 | 38 | func testViewDidLoad() { 39 | // assuming, that viewDidLoad() was called 40 | // due to line `_ = viewController.view` 41 | 42 | // when 43 | viewController.customView.didPressSubmitButton?() 44 | 45 | // then 46 | XCTAssertNotNil(viewController.customView.didPressSubmitButton) 47 | XCTAssertEqual(viewController.navigationItem.title, "Recover password") 48 | XCTAssertEqual(presenter.handleSubmitButtonTapCounter, 1) 49 | } 50 | 51 | func testGetEmailText() { 52 | // given 53 | let text = "email" 54 | viewController.customView.emailTextField.text = text 55 | // when 56 | let fetchedText = viewController.getEmailText() 57 | // then 58 | XCTAssertEqual(text, fetchedText) 59 | } 60 | } 61 | 62 | class MockForgotPasswordPresenter: ForgotPasswordPresentation { 63 | var handleSubmitButtonTapCounter = 0 64 | 65 | func handleSubmitButtonTap() { 66 | handleSubmitButtonTapCounter += 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/SignIn/SignInConfiguratorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInConfiguratorTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/21/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT - SignInConfigurator 14 | /// 15 | 16 | class SignInConfiguratorTests: XCTestCase { 17 | 18 | var signInConfigurator: SignInConfigurator! 19 | 20 | override func setUp() { 21 | super.setUp() 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | signInConfigurator = SignInConfigurator() 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | signInConfigurator = nil 29 | super.tearDown() 30 | } 31 | 32 | func testCorrectSceneBindings() { 33 | // when 34 | let viewController = signInConfigurator.configuredViewController(delegate: nil) 35 | // then 36 | XCTAssertNotNil(viewController.presenter, "ViewController's presenter must be not nil after bindings.") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NiceDemoTests/Scenes/SignIn/SignInSceneTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInSceneTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/21/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: SignInScene: SignInViewController, SignInView, SignInPresenter 14 | /// 15 | 16 | /// Used for testing purpose only. 17 | class MockUserDefaultsLayer: UserDefaultsLayer { 18 | override init(userDefaultsSuiteName: String?) { 19 | if let name = userDefaultsSuiteName { 20 | // removing the persistent domain for suit name 21 | UserDefaults().removePersistentDomain(forName: name) 22 | } 23 | super.init(userDefaultsSuiteName: userDefaultsSuiteName) 24 | } 25 | } 26 | 27 | class SignInSceneTests: XCTestCase { 28 | 29 | var viewController: SignInViewController! 30 | var presenter: SignInPresenter! 31 | var customView: SignInView! 32 | var userCredentialsStorageService: UserCredentialsStorageService! 33 | 34 | override func setUp() { 35 | super.setUp() 36 | // Put setup code here. This method is called before the invocation of each test method in the class. 37 | // MVP bingings 38 | userCredentialsStorageService = UserCredentialsStorageService(storage: MockUserDefaultsLayer(userDefaultsSuiteName: "TestUserDefaultsLayer")) 39 | userCredentialsStorageService.isUserAuthenticated = false 40 | viewController = SignInViewController() 41 | presenter = SignInPresenter(view: viewController, userCredentialsStorage: userCredentialsStorageService) 42 | viewController.setPresenter(presenter) 43 | } 44 | 45 | override func tearDown() { 46 | // Put teardown code here. This method is called after the invocation of each test method in the class. 47 | viewController = nil 48 | presenter = nil 49 | customView = nil 50 | userCredentialsStorageService = nil 51 | super.tearDown() 52 | } 53 | 54 | /// Testing correct credentials on input. 55 | func testUserCredentialsFieldIsUpdatedAfterSuccessfulAuthentication() { 56 | // when 57 | viewController.setEmailText("test@test.com") // correct format 58 | viewController.setPasswordText("123456") // correct format 59 | presenter.handleSignInButtonTap() 60 | // then 61 | XCTAssertTrue(userCredentialsStorageService.isUserAuthenticated, "Must be true, as user's credentials are correct.") 62 | } 63 | 64 | /// Testing wrong credentials on input. 65 | func testUserCredentialsFieldIsUpdatedAfterFailedAuthentication() { 66 | // when 67 | viewController.setEmailText("wrong_email") // uncorrect format 68 | viewController.setPasswordText("123") // uncorrect format 69 | presenter.handleSignInButtonTap() 70 | // then 71 | XCTAssertFalse(userCredentialsStorageService.isUserAuthenticated, "Must be false, as user's credentials are incorrect.") 72 | } 73 | 74 | } 75 | 76 | -------------------------------------------------------------------------------- /NiceDemoTests/Validator/ValidatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValidatorTests.swift 3 | // NiceDemoTests 4 | // 5 | // Created by Serhii Kharauzov on 3/23/18. 6 | // Copyright © 2018 Serhii Kharauzov. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NiceDemo 11 | 12 | /// 13 | /// SUT: Validator 14 | /// 15 | 16 | class ValidatorTests: XCTestCase { 17 | 18 | var validator: Validator! 19 | 20 | override func setUp() { 21 | super.setUp() 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | validator = Validator() 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | validator = nil 29 | super.tearDown() 30 | } 31 | 32 | func testCorrectValidationOfEmail() { 33 | // when 34 | XCTAssertThrowsError(try validator.validateEmail("wrongEmail")) { error in 35 | // then 36 | XCTAssertEqual(error as? Validator.ValidationError, Validator.ValidationError.emailFormatIsWrong, "") 37 | } 38 | 39 | // when 40 | XCTAssertThrowsError(try validator.validateEmail(nil)) { error in 41 | // then 42 | XCTAssertEqual(error as? Validator.ValidationError, Validator.ValidationError.valueIsEmpty("Email"), "") 43 | } 44 | 45 | // when 46 | XCTAssertThrowsError(try validator.validateEmail("")) { error in 47 | // then 48 | XCTAssertEqual(error as? Validator.ValidationError, Validator.ValidationError.valueIsEmpty("Email"), "") 49 | } 50 | 51 | // when 52 | // then 53 | XCTAssertNoThrow(try validator.validateEmail("test@gmail.com")) 54 | } 55 | 56 | func testCorrectValidationOfPassword() { 57 | // when 58 | XCTAssertThrowsError(try validator.validatePassword("111")) { error in 59 | // then 60 | XCTAssertEqual(error as? Validator.ValidationError, Validator.ValidationError.passwordLengthIsWrong, "") 61 | } 62 | 63 | // when 64 | XCTAssertThrowsError(try validator.validatePassword(nil)) { error in 65 | // then 66 | XCTAssertEqual(error as? Validator.ValidationError, Validator.ValidationError.valueIsEmpty("Password"), "") 67 | } 68 | 69 | // when 70 | XCTAssertThrowsError(try validator.validatePassword("")) { error in 71 | // then 72 | XCTAssertEqual(error as? Validator.ValidationError, Validator.ValidationError.valueIsEmpty("Password"), "") 73 | } 74 | 75 | // when 76 | // then 77 | XCTAssertNoThrow(try validator.validatePassword("goodPassword")) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Swift](https://img.shields.io/badge/Swift-5.0-orange.svg) 2 | ![Platform](https://img.shields.io/badge/platform-iOS-lightgrey.svg) 3 | ![BuildPass](https://img.shields.io/badge/build-passing-brightgreen.svg) 4 | ![Codecoverage](https://img.shields.io/badge/coverage-78%25-yellow.svg) 5 | [![License](https://img.shields.io/badge/license-mit-blue.svg)](https://doge.mit-license.org) 6 | 7 | # Sense 8 | *Own blueprint*. 9 | 10 | ## Foundation 11 | - *Сleanness* 💎 - Project is built using SOLID principles. 12 | - *Stability* ✊ - Coordinator for routing + improved MVP for scenes. 13 | - *Transparency* 👓 - No third-party frameworks and Pods. 14 | - *Reuse* 🤹‍ - UI is implemented programmatically too. No Storyboard or Xibs. 15 | - *Testable* 🔒 - Code is fully covered by Unit Tests. 16 | 17 | ## Overview 18 | This project was built for demonstration purpose using iOS best practices. It doesn't have or provide any commercial usage. 19 | Auth flow only imitates user's authentication/actions. 20 | 21 | App uses [Dog-api](https://dog.ceo/dog-api/) for backend. It is free and amazing 🐕. 22 | 23 | You might notice that this app doesn't have a fancy UI. That's because the project is concentrated on architecture and patterns rather than interfaces. 24 | If you're looking for complex interfaces along with smooth animations, please check my other projects here: [CardsHolder](https://github.com/Kharauzov/CardsHolder) or [SwipeableCards](https://github.com/Kharauzov/SwipeableCards). 25 | 26 | ## User Interfaces 27 | 28 |

29 | 30 |

31 | 32 | ## Navigation scheme 33 | Illustrates all flows of the project: 34 | - Starting flow 35 | - Auth flow 36 | - Dogs flow 37 | 38 | 39 |

40 | 41 |

42 | 43 | ## Feedback 44 | If you have any questions or suggestions, feel free to open issue just at this project. 45 | 46 | ## License 47 | NiceDemo and all its classes are available under the MIT license. See the LICENSE file for more info. 48 | -------------------------------------------------------------------------------- /VisualFiles/NiceDemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/VisualFiles/NiceDemo.png -------------------------------------------------------------------------------- /VisualFiles/NiceDemo.xml: -------------------------------------------------------------------------------- 1 | 7Vxbd5s4EP41fkwOQlwf49jO9px2mz3Znm0esZExW4xYkOu4v34lkAxC8iUNDo4TvwRGQoj55psZjdQO4O3y6S4PssUXHKJkYBrh0wCOBib92Qb9wyQbLjFcq5JEeRxWMlALHuJfiAv5g9EqDlEhdSQYJyTOZOEMpymaEUkW5Dley93mOJHfmgURUgQPsyBRpf/EIVlUUk98F5P/geJoId4MDN4yDWY/ohyvUv6+gQnn5a9qXgZiLN6/WAQhXjdEcDyAtznGpLpaPt2ihClXqK16brKjdTvvHKXkmAccPg2yEZ+OQqoJfotzssARToNkXEuH5echNoBB7xZkmdBLQC//RYRsOJjBimAqqkf4jHHG+81xSng3YLP7OElucYLzcgYwtJEXWlRekBz/QI0Wz5xCx6EtUR6EMf3ERpvvhobrbp8SsFGNDlWlcD0VeJXP+Heb3MyCPEK8l12JmEYaj3FF3iG8RCTf0A45SgIS/5RtJ+AmGG371TDQC46EHhU+l59BskLCjlowUcPJ2OVqmdzMCFPC8CfKSUxt+HMwRck9LmIS45R2mWJC8JJ2SFjDcGuhDeVxG63HuEniiD1LGGhNjPGKJHFKQRHEMzoFdBTndNhq3mtUEPaQoIihBXefKewEnn0metoLKm+FnKjCk3n8fl27BdFl0fAIjvFyKwBHmMF7ZautshWAvuhqa3ByEsJ1R68jUn5XJZsKwU2W3WKch3EaMALzZvqyafsRKpNHallBjTEDbL2ICXrIglJXaxqhZdwP4Aldx3KRgmeKU6SBkouDfCYNiSWw5xCZ03k3fATmYUIC+1SM7DRcUiXkm+9cXt48sptr+wXkRE8x+b4dhd09Ch99DrR1Vdr6fbEW+Apt/8QKvtQsiQxbjor4VzAtOzDFZjhOSTkPeziwR4wNPHjOqCpQMzKLqLqMw7C0jSMichthjZ6dHSTa5sd8tlKOqSPXFTUbR0QZTrArnrkfrX8++j1TS6MLns8LinkboO0kjsLMPeBpG8g5/62waLgqSgXe0A7AyZ7qxp2++VNB+64Kih5TGW/MRWtBYaSMNp1gyZxrOi3Yn5hCO3mRG1/g5XRVvNiFh8HUcEIdt0PXnxrGbj+uS8NS5nhUp264Dgp2GORznbrdduq2qTp1CFSnDoDdQbZt951WOap62W8y0Xrabj35UX7bV/22afXluFW/3XYCop4gaA72Mv4Ip2HrnMbNiiw+Erjnct2BcnwBQJPAuadK4KDGdj6WVNL6qUlyt7fkDChAPdCJMXY/0LQKqaglSZwV6DfYI5u65Vljx1IxGftj89bUYTL0hqbvd1Rt6HN1s61JvrHVDTgXBmlWN8K/dMegVnotMihf9qo2tK8t6Nc/ecBq1nyMF2XkpkrUR1QohvQGllHC/N/BMgp658z0s8+GBbRNosPeQqWYjSYfVvLQZhI6wlExSfBan8LqKpdyZnvGKax/6hQWSNy2LF0K658oSutSWGn5M9t+db1igWYIDRg0RUcseyy67Cnhb618xmmlj6xyTnuNhlVNXvCqIyKJ1kiaFiUM5Oh4orNe2f01Ddja4686sDZg2ZK1QVtjbYYmJzS7sDb3rUaK3ZXw84ghliZZdPqKIcKhXQrM4Kxh9nqD2XqrMJ8tmBD2lvdZSiLwQOPYp/TCaySeJ9dItAXEU9VIYKd7BTWDKu+15dDFMsg5KwaJjVOZQd+yC2cQMKxWmfE1q4yeovQJziNM7oOiWNOl6HtTPvCdV/RfvR/43OejzqKK270/0pdsgS+v60y3VaStnKRSpL3J82DT6MYLp7vfA/ncJgcmUptQ9Yrfri4erFAcVw5QagEPq9kMFcV8xd5G7W1BYaTr9/KcwpEbreLYbJZjNtTzfInGdIHheL5mAwLZYRhCnen6vm95o458Sft4qjYXMk/kS6xO94suMt+Bmi0h2NuJN6gen/ocF+TrnJWCTxB2W5TwkTnTh9fR0PVHGn2LWp4Sv1EZvztgUHsxoS3lniwYqydZKBJ3QZKgco7vEA9gtKLiqwJiqZuZf28yViJmL6PjMSRmOULpu65u872ETgD3DRlvWxfBTlXdtswuI1hdinTbJefGQt89+Ur/qMgkTL0ZmYTs9SOTpdayauIlOIrTD+Z1zTzPkXcxgatZh56KeWNz+fgXsu4f18uvv75dBdEXBK/sg8e7u93aVIziW3ngu6vtzAuxuZZf0RyPUcxw98mYHrcy9SanVqMUlGRtHUi6NP9aUdKlYZQri4Ork+0Cck8OdoyjwHSmMdnwCTGD+ypJugB1u+oUlQz/FVehelh1Z8QvF9YOQASW0QLR9a/tnmEUx8DfCYwnYadrGtey170ChnkNbdO3AGAHrQyRCPSHs7ru+sD5mTg7tpzO2b5xDSi4tueZJdgnw5je1v9hRVUvrv9bEDj+Hw== -------------------------------------------------------------------------------- /VisualFiles/NiceDemoUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharauzov/NiceDemo/0d594139558dc7493b770591e8bbf089c23c44d1/VisualFiles/NiceDemoUI.png --------------------------------------------------------------------------------