├── .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 |
22 |
23 |
24 |
25 |
26 |
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 | 
2 | 
3 | 
4 | 
5 | [](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
--------------------------------------------------------------------------------