├── MoviesApp ├── Protocols │ ├── Coordinator.swift │ ├── Presentable.swift │ ├── AlertShowable.swift │ ├── IndicatorShowable.swift │ └── Refreshable.swift ├── Assets.xcassets │ ├── Contents.json │ ├── cat3.imageset │ │ ├── cat3.png │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── en.lproj │ └── Localizable.strings ├── Base.lproj │ ├── Localizable.strings │ ├── LaunchScreen.storyboard │ └── SearchScreen.storyboard ├── Features │ ├── MovieList │ │ ├── MovieListView.swift │ │ ├── MovieListRepository.swift │ │ ├── MovieListViewController.swift │ │ ├── MovieListCell.swift │ │ ├── MovieListPresenter.swift │ │ ├── MovieViewModel.swift │ │ └── MovieListScreen.storyboard │ └── Search │ │ ├── SearchView.swift │ │ ├── SuggestionCell.swift │ │ ├── Storage.swift │ │ ├── SearchRepository.swift │ │ ├── SearchPresenter.swift │ │ └── SearchViewController.swift ├── URL.swift ├── Extensions │ └── StringExtension.swift ├── ViewController.swift ├── Movie.swift ├── Coordinator │ ├── Router.swift │ ├── AppCoordinator.swift │ └── Factory.swift ├── AppDelegate.swift └── Info.plist ├── MoviesAppTests ├── Slate │ ├── Invoke.swift │ ├── XCTestCaseExtensions.swift │ ├── Mock.swift │ ├── Equivalent.swift │ ├── BaseTestCase.swift │ └── CallHandler.swift ├── Test environment │ ├── RepositoryState.swift │ └── Builders │ │ ├── SuggestionBuilder.swift │ │ └── MovieBuilder.swift ├── Info.plist └── Features │ ├── MovieList │ ├── MovieListRepositoryMock.swift │ ├── MovieListViewMock.swift │ └── MovieListPresenterTest.swift │ └── Search │ ├── SearchRepositoryMock.swift │ ├── SearchViewMock.swift │ └── SearchPresenterTest.swift ├── Pods ├── Target Support Files │ ├── Unbox │ │ ├── Unbox.modulemap │ │ ├── Unbox-dummy.m │ │ ├── Unbox-prefix.pch │ │ ├── Unbox-umbrella.h │ │ ├── Unbox.xcconfig │ │ └── Info.plist │ ├── TableKit │ │ ├── TableKit.modulemap │ │ ├── TableKit-dummy.m │ │ ├── TableKit-prefix.pch │ │ ├── TableKit-umbrella.h │ │ ├── TableKit.xcconfig │ │ └── Info.plist │ ├── Alamofire │ │ ├── Alamofire.modulemap │ │ ├── Alamofire-dummy.m │ │ ├── Alamofire-prefix.pch │ │ ├── Alamofire-umbrella.h │ │ ├── Alamofire.xcconfig │ │ └── Info.plist │ ├── MBProgressHUD │ │ ├── MBProgressHUD.modulemap │ │ ├── MBProgressHUD-dummy.m │ │ ├── MBProgressHUD-prefix.pch │ │ ├── MBProgressHUD-umbrella.h │ │ ├── MBProgressHUD.xcconfig │ │ └── Info.plist │ ├── Pods-MoviesApp │ │ ├── Pods-MoviesApp.modulemap │ │ ├── Pods-MoviesApp-dummy.m │ │ ├── Pods-MoviesApp-umbrella.h │ │ ├── Info.plist │ │ ├── Pods-MoviesApp.debug.xcconfig │ │ ├── Pods-MoviesApp.release.xcconfig │ │ ├── Pods-MoviesApp-frameworks.sh │ │ ├── Pods-MoviesApp-resources.sh │ │ ├── Pods-MoviesApp-acknowledgements.markdown │ │ └── Pods-MoviesApp-acknowledgements.plist │ └── SwiftyUserDefaults │ │ ├── SwiftyUserDefaults.modulemap │ │ ├── SwiftyUserDefaults-dummy.m │ │ ├── SwiftyUserDefaults-prefix.pch │ │ ├── SwiftyUserDefaults-umbrella.h │ │ ├── SwiftyUserDefaults.xcconfig │ │ └── Info.plist ├── Unbox │ ├── Sources │ │ ├── UnboxPathNode.swift │ │ ├── NSDictionary+Unbox.swift │ │ ├── Typealiases.swift │ │ ├── DateFormatter+Unbox.swift │ │ ├── NSArray+Unbox.swift │ │ ├── URL+Unbox.swift │ │ ├── UnboxableKey.swift │ │ ├── Int+Unbox.swift │ │ ├── CGFloat+Unbox.swift │ │ ├── UInt+Unbox.swift │ │ ├── Float+Unbox.swift │ │ ├── Int32+Unbox.swift │ │ ├── Int64+Unbox.swift │ │ ├── Sequence+Unbox.swift │ │ ├── String+Unbox.swift │ │ ├── Double+Unbox.swift │ │ ├── UInt32+Unbox.swift │ │ ├── UInt64+Unbox.swift │ │ ├── UnboxPath.swift │ │ ├── JSONSerialization+Unbox.swift │ │ ├── Decimal+Unbox.swift │ │ ├── UnboxableEnum.swift │ │ ├── UnboxContainer.swift │ │ ├── Unboxable.swift │ │ ├── Optional+Unbox.swift │ │ ├── Set+Unbox.swift │ │ ├── Bool+Unbox.swift │ │ ├── UnboxArrayContainer.swift │ │ ├── UnboxCollectionElementTransformer.swift │ │ ├── UnboxableByTransform.swift │ │ ├── UnboxCompatible.swift │ │ ├── UnboxableRawType.swift │ │ ├── UnboxError.swift │ │ ├── Array+Unbox.swift │ │ ├── Data+Unbox.swift │ │ ├── UnboxFormatter.swift │ │ ├── UnboxableWithContext.swift │ │ ├── UnboxPathError.swift │ │ ├── UnboxableCollection.swift │ │ ├── Dictionary+Unbox.swift │ │ └── Unbox.swift │ └── LICENSE ├── Manifest.lock ├── MBProgressHUD │ ├── LICENSE │ └── README.mdown ├── TableKit │ ├── LICENSE │ ├── Sources │ │ ├── ConfigurableCell.swift │ │ ├── Operators.swift │ │ ├── TableCellAction.swift │ │ ├── TableCellRegisterer.swift │ │ ├── TableKit.swift │ │ ├── TableRowAction.swift │ │ ├── TablePrototypeCellHeightCalculator.swift │ │ ├── TableSection.swift │ │ └── TableRow.swift │ └── README.md ├── Alamofire │ ├── LICENSE │ └── Source │ │ ├── DispatchQueue+Alamofire.swift │ │ ├── Notifications.swift │ │ └── Timeline.swift └── SwiftyUserDefaults │ └── LICENSE ├── MoviesApp.xcodeproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── Podfile ├── MoviesApp.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── MoviesApp.xcscmblueprint ├── Podfile.lock ├── .gitignore └── README.md /MoviesApp/Protocols/Coordinator.swift: -------------------------------------------------------------------------------- 1 | protocol Coordinator { 2 | func start() 3 | } 4 | -------------------------------------------------------------------------------- /MoviesAppTests/Slate/Invoke.swift: -------------------------------------------------------------------------------- 1 | enum Invoke { 2 | case once, never, times(UInt) 3 | } 4 | -------------------------------------------------------------------------------- /MoviesApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MoviesApp/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreyPanov/MoviesApp/HEAD/MoviesApp/en.lproj/Localizable.strings -------------------------------------------------------------------------------- /MoviesApp/Base.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreyPanov/MoviesApp/HEAD/MoviesApp/Base.lproj/Localizable.strings -------------------------------------------------------------------------------- /MoviesApp/Assets.xcassets/cat3.imageset/cat3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreyPanov/MoviesApp/HEAD/MoviesApp/Assets.xcassets/cat3.imageset/cat3.png -------------------------------------------------------------------------------- /MoviesAppTests/Test environment/RepositoryState.swift: -------------------------------------------------------------------------------- 1 | enum RepositoryMockState { 2 | case success, successWith(T), failure, empty, failWith(String) 3 | } 4 | -------------------------------------------------------------------------------- /MoviesApp/Features/MovieList/MovieListView.swift: -------------------------------------------------------------------------------- 1 | protocol MovieListView: Presentable, Refreshable, AlertShowable { 2 | func show(movies: [MovieViewModel]) 3 | } 4 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Unbox/Unbox.modulemap: -------------------------------------------------------------------------------- 1 | framework module Unbox { 2 | umbrella header "Unbox-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/TableKit/TableKit.modulemap: -------------------------------------------------------------------------------- 1 | framework module TableKit { 2 | umbrella header "TableKit-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Unbox/Unbox-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Unbox : NSObject 3 | @end 4 | @implementation PodsDummy_Unbox 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Alamofire/Alamofire.modulemap: -------------------------------------------------------------------------------- 1 | framework module Alamofire { 2 | umbrella header "Alamofire-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/TableKit/TableKit-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_TableKit : NSObject 3 | @end 4 | @implementation PodsDummy_TableKit 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Alamofire/Alamofire-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Alamofire : NSObject 3 | @end 4 | @implementation PodsDummy_Alamofire 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MBProgressHUD/MBProgressHUD.modulemap: -------------------------------------------------------------------------------- 1 | framework module MBProgressHUD { 2 | umbrella header "MBProgressHUD-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MoviesApp/Pods-MoviesApp.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_MoviesApp { 2 | umbrella header "Pods-MoviesApp-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MBProgressHUD/MBProgressHUD-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_MBProgressHUD : NSObject 3 | @end 4 | @implementation PodsDummy_MBProgressHUD 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MoviesApp/Pods-MoviesApp-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_MoviesApp : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_MoviesApp 5 | @end 6 | -------------------------------------------------------------------------------- /MoviesApp/URL.swift: -------------------------------------------------------------------------------- 1 | struct URL { 2 | static let forRequest = "http://api.themoviedb.org/3/search/movie?api_key=2696829a81b1b5827d515ff121700838&query=" 3 | static let forImage = "http://image.tmdb.org/t/p/w92" 4 | } 5 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyUserDefaults/SwiftyUserDefaults.modulemap: -------------------------------------------------------------------------------- 1 | framework module SwiftyUserDefaults { 2 | umbrella header "SwiftyUserDefaults-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyUserDefaults/SwiftyUserDefaults-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_SwiftyUserDefaults : NSObject 3 | @end 4 | @implementation PodsDummy_SwiftyUserDefaults 5 | @end 6 | -------------------------------------------------------------------------------- /MoviesApp/Features/Search/SearchView.swift: -------------------------------------------------------------------------------- 1 | protocol SearchView: IndicatorShowable, AlertShowable, Presentable { 2 | 3 | var onMoviesSelected: (([Movie]) -> Void)? { get set } 4 | 5 | func show(suggestions: [String]) 6 | } 7 | -------------------------------------------------------------------------------- /MoviesApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MoviesAppTests/Slate/XCTestCaseExtensions.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func verify(_ mock: T, _ invoked: Invoke = .once) -> T.InstanceType { 6 | return mock.verify(invoked: invoked) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /MoviesApp/Features/Search/SuggestionCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import TableKit 3 | 4 | class SuggestionCell: UITableViewCell, ConfigurableCell { 5 | 6 | func configure(with suggestion: String) { 7 | textLabel?.text = suggestion 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | project 'MoviesApp.xcodeproj' 2 | 3 | platform :ios, '10.0' 4 | 5 | target 'MoviesApp' do 6 | use_frameworks! 7 | pod 'TableKit' 8 | pod 'Unbox' 9 | pod 'MBProgressHUD' 10 | pod 'Alamofire' 11 | pod 'SwiftyUserDefaults' 12 | 13 | end 14 | -------------------------------------------------------------------------------- /MoviesApp/Protocols/Presentable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol Presentable { 4 | func toPresent() -> UIViewController? 5 | } 6 | 7 | extension UIViewController: Presentable { 8 | 9 | func toPresent() -> UIViewController? { 10 | return self 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Unbox/Unbox-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Alamofire/Alamofire-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/TableKit/TableKit-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxPathNode.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | internal protocol UnboxPathNode { 10 | func unboxPathValue(forKey key: String) -> Any? 11 | } 12 | -------------------------------------------------------------------------------- /MoviesAppTests/Test environment/Builders/SuggestionBuilder.swift: -------------------------------------------------------------------------------- 1 | class SuggestionBuilder { 2 | 3 | static func suggestion() -> String { 4 | return "Batman" 5 | } 6 | 7 | static func suggestions() -> [String] { 8 | return [ 9 | "Batman", "Robin", "Clone" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MBProgressHUD/MBProgressHUD-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /MoviesApp.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyUserDefaults/SwiftyUserDefaults-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /MoviesApp/Extensions/StringExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | 5 | var localized: String { 6 | return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "") 7 | } 8 | 9 | var trimmed: String { 10 | return self.replacingOccurrences(of: " ", with: "+") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/NSDictionary+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | #if !os(Linux) 10 | extension NSDictionary: UnboxPathNode { 11 | func unboxPathValue(forKey key: String) -> Any? { 12 | return self[key] 13 | } 14 | } 15 | #endif 16 | -------------------------------------------------------------------------------- /MoviesApp/Features/MovieList/MovieListRepository.swift: -------------------------------------------------------------------------------- 1 | class MovieListRepository: SearchRepository { 2 | 3 | func repeatLastSearch(onSuccess: @escaping ([Movie]) -> Void, onError: @escaping (String) -> Void) { 4 | guard let lastSearch = storage.getLastSuggestion() else { onError("not_found".localized); return } 5 | searchMovies(with: lastSearch, onSuccess: onSuccess, onError: onError) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Typealiases.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Type alias defining what type of Dictionary that is Unboxable (valid JSON) 10 | public typealias UnboxableDictionary = [String : Any] 11 | 12 | internal typealias UnboxTransform = (Any) throws -> T? 13 | -------------------------------------------------------------------------------- /MoviesAppTests/Slate/Mock.swift: -------------------------------------------------------------------------------- 1 | protocol Mock { 2 | 3 | associatedtype InstanceType 4 | var callHandler: CallHandler { get } 5 | 6 | func instanceType() -> InstanceType 7 | func verify(invoked: Invoke) -> InstanceType 8 | } 9 | 10 | extension Mock { 11 | 12 | func verify(invoked: Invoke) -> InstanceType { 13 | callHandler.verify(invoked: invoked) 14 | return instanceType() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Unbox/Unbox-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double UnboxVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char UnboxVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/TableKit/TableKit-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double TableKitVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char TableKitVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Alamofire/Alamofire-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double AlamofireVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char AlamofireVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /MoviesApp/Assets.xcassets/cat3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat3.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MoviesApp/Pods-MoviesApp-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_MoviesAppVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_MoviesAppVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/DateFormatter+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | 8 | import Foundation 9 | 10 | /// Extension making `DateFormatter` usable as an UnboxFormatter 11 | extension DateFormatter: UnboxFormatter { 12 | public func format(unboxedValue: String) -> Date? { 13 | return self.date(from: unboxedValue) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/NSArray+Unbox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSArray+UnboxPathNode.swift 3 | // Unbox 4 | // 5 | // Created by John Sundell on 2017-03-27. 6 | // Copyright © 2017 John Sundell. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if !os(Linux) 12 | extension NSArray: UnboxPathNode { 13 | func unboxPathValue(forKey key: String) -> Any? { 14 | return (self as Array).unboxPathValue(forKey: key) 15 | } 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyUserDefaults/SwiftyUserDefaults-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double SwiftyUserDefaultsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char SwiftyUserDefaultsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MBProgressHUD/MBProgressHUD-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | #import "MBProgressHUD.h" 14 | 15 | FOUNDATION_EXPORT double MBProgressHUDVersionNumber; 16 | FOUNDATION_EXPORT const unsigned char MBProgressHUDVersionString[]; 17 | 18 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/URL+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `URL` Unboxable by transform 10 | extension URL: UnboxableByTransform { 11 | public typealias UnboxRawValue = String 12 | 13 | public static func transform(unboxedValue: String) -> URL? { 14 | return URL(string: unboxedValue) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxableKey.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used to enable any type to be transformed from a JSON key into a dictionary key 10 | public protocol UnboxableKey { 11 | /// Transform an unboxed key into a key that will be used in an unboxed dictionary 12 | static func transform(unboxedKey: String) -> Self? 13 | } 14 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Int+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `Int` an Unboxable raw type 10 | extension Int: UnboxableRawType { 11 | public static func transform(unboxedNumber: NSNumber) -> Int? { 12 | return unboxedNumber.intValue 13 | } 14 | 15 | public static func transform(unboxedString: String) -> Int? { 16 | return Int(unboxedString) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MoviesApp/Protocols/AlertShowable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol AlertShowable: class { 4 | func show(message text: String) 5 | } 6 | 7 | extension AlertShowable where Self: UIViewController { 8 | 9 | func show(message text: String) { 10 | let alertController = UIAlertController(title: nil, message: text, preferredStyle: .alert) 11 | let okAction = UIAlertAction(title: "Ok", style: .cancel) { _ in } 12 | alertController.addAction(okAction) 13 | present(alertController, animated: true, completion: nil) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/CGFloat+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | #if !os(Linux) 10 | import CoreGraphics 11 | 12 | /// Extension making `CGFloat` an Unboxable raw type 13 | extension CGFloat: UnboxableByTransform { 14 | public typealias UnboxRawValue = Double 15 | 16 | public static func transform(unboxedValue: Double) -> CGFloat? { 17 | return CGFloat(unboxedValue) 18 | } 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UInt+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making UInt an Unboxable raw type 10 | extension UInt: UnboxableRawType { 11 | public static func transform(unboxedNumber: NSNumber) -> UInt? { 12 | return unboxedNumber.uintValue 13 | } 14 | 15 | public static func transform(unboxedString: String) -> UInt? { 16 | return UInt(unboxedString) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Float+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `Float` an Unboxable raw type 10 | extension Float: UnboxableRawType { 11 | public static func transform(unboxedNumber: NSNumber) -> Float? { 12 | return unboxedNumber.floatValue 13 | } 14 | 15 | public static func transform(unboxedString: String) -> Float? { 16 | return Float(unboxedString) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Int32+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `Int32` an Unboxable raw type 10 | extension Int32: UnboxableRawType { 11 | public static func transform(unboxedNumber: NSNumber) -> Int32? { 12 | return unboxedNumber.int32Value 13 | } 14 | 15 | public static func transform(unboxedString: String) -> Int32? { 16 | return Int32(unboxedString) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Int64+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `Int64` an Unboxable raw type 10 | extension Int64: UnboxableRawType { 11 | public static func transform(unboxedNumber: NSNumber) -> Int64? { 12 | return unboxedNumber.int64Value 13 | } 14 | 15 | public static func transform(unboxedString: String) -> Int64? { 16 | return Int64(unboxedString) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Sequence+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension Sequence { 10 | func map(allowInvalidElements: Bool, transform: (Iterator.Element) throws -> T) throws -> [T] { 11 | if !allowInvalidElements { 12 | return try self.map(transform) 13 | } 14 | 15 | return self.flatMap { 16 | return try? transform($0) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/String+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `String` an Unboxable raw type 10 | extension String: UnboxableRawType { 11 | public static func transform(unboxedNumber: NSNumber) -> String? { 12 | return unboxedNumber.stringValue 13 | } 14 | 15 | public static func transform(unboxedString: String) -> String? { 16 | return unboxedString 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Double+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `Double` an Unboxable raw type 10 | extension Double: UnboxableRawType { 11 | public static func transform(unboxedNumber: NSNumber) -> Double? { 12 | return unboxedNumber.doubleValue 13 | } 14 | 15 | public static func transform(unboxedString: String) -> Double? { 16 | return Double(unboxedString) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UInt32+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `UInt32` an Unboxable raw type 10 | extension UInt32: UnboxableRawType { 11 | public static func transform(unboxedNumber: NSNumber) -> UInt32? { 12 | return unboxedNumber.uint32Value 13 | } 14 | 15 | public static func transform(unboxedString: String) -> UInt32? { 16 | return UInt32(unboxedString) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UInt64+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `UInt64` an Unboxable raw type 10 | extension UInt64: UnboxableRawType { 11 | public static func transform(unboxedNumber: NSNumber) -> UInt64? { 12 | return unboxedNumber.uint64Value 13 | } 14 | 15 | public static func transform(unboxedString: String) -> UInt64? { 16 | return UInt64(unboxedString) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxPath.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | internal enum UnboxPath { 10 | case key(String) 11 | case keyPath(String) 12 | } 13 | 14 | extension UnboxPath: CustomStringConvertible { 15 | var description: String { 16 | switch self { 17 | case .key(let key): 18 | return key 19 | case .keyPath(let keyPath): 20 | return keyPath 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/JSONSerialization+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension JSONSerialization { 10 | static func unbox(data: Data, options: ReadingOptions = []) throws -> T { 11 | do { 12 | return try (self.jsonObject(with: data, options: options) as? T).orThrow(UnboxError.invalidData) 13 | } catch { 14 | throw UnboxError.invalidData 15 | } 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /MoviesApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import TableKit 3 | 4 | class ViewController: UIViewController, Refreshable { 5 | 6 | var refreshControl: UIRefreshControl? 7 | @IBOutlet weak var tableView: UITableView! { 8 | didSet { 9 | tableDirector = TableDirector(tableView: tableView) 10 | tableView.rowHeight = UITableViewAutomaticDimension 11 | } 12 | } 13 | var tableDirector: TableDirector! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | view.accessibilityIdentifier = String(describing: type(of: self)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Decimal+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `Decimal` an Unboxable raw type 10 | extension Decimal: UnboxableRawType { 11 | public static func transform(unboxedNumber: NSNumber) -> Decimal? { 12 | return Decimal(string: unboxedNumber.stringValue) 13 | } 14 | 15 | public static func transform(unboxedString unboxedValue: String) -> Decimal? { 16 | return Decimal(string: unboxedValue) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxableEnum.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used to enable an enum to be directly unboxable 10 | public protocol UnboxableEnum: RawRepresentable, UnboxCompatible {} 11 | 12 | /// Default implementation of `UnboxCompatible` for enums 13 | public extension UnboxableEnum { 14 | static func unbox(value: Any, allowInvalidCollectionElements: Bool) throws -> Self? { 15 | return (value as? RawValue).map(self.init) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MoviesApp/Protocols/IndicatorShowable.swift: -------------------------------------------------------------------------------- 1 | import MBProgressHUD 2 | 3 | protocol IndicatorShowable: class { 4 | func showLoadingIndicator() 5 | func hideLoadingIndicator() 6 | } 7 | 8 | extension IndicatorShowable where Self: UIViewController { 9 | 10 | func showLoadingIndicator() { 11 | let indicator = MBProgressHUD.showAdded(to: self.view, animated: true) 12 | indicator.mode = .indeterminate 13 | indicator.show(animated: true) 14 | } 15 | 16 | func hideLoadingIndicator() { 17 | let indicator = MBProgressHUD(for: self.view) 18 | indicator?.hide(animated: false) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Pods/Target Support Files/TableKit/TableKit.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/TableKit 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" 4 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 5 | PODS_BUILD_DIR = $BUILD_DIR 6 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/TableKit 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxContainer.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | internal struct UnboxContainer: UnboxableWithContext { 10 | let model: T 11 | 12 | init(unboxer: Unboxer, context: UnboxPath) throws { 13 | switch context { 14 | case .key(let key): 15 | self.model = try unboxer.unbox(key: key) 16 | case .keyPath(let keyPath): 17 | self.model = try unboxer.unbox(keyPath: keyPath) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Alamofire/Alamofire.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/Alamofire 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" 4 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 5 | PODS_BUILD_DIR = $BUILD_DIR 6 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/Alamofire 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /MoviesApp/Movie.swift: -------------------------------------------------------------------------------- 1 | import Unbox 2 | 3 | struct Movie: Unboxable { 4 | let title: String 5 | let releaseDate: String? 6 | let overview: String? 7 | let imagePath: String? 8 | 9 | init(unboxer: Unboxer) throws { 10 | title = try unboxer.unbox(key: "title") 11 | releaseDate = try? unboxer.unbox(key: "release_date") 12 | overview = try? unboxer.unbox(key: "overview") 13 | imagePath = try? unboxer.unbox(key: "backdrop_path") 14 | } 15 | 16 | static func deserialize(with jsonArray: [[String: AnyObject]]) -> [Movie]? { 17 | return try? unbox(dictionaries: jsonArray, allowInvalidElements: true) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Unboxable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used to declare a model as being Unboxable, for use with the unbox() function 10 | public protocol Unboxable { 11 | /// Initialize an instance of this model by unboxing a dictionary using an Unboxer 12 | init(unboxer: Unboxer) throws 13 | } 14 | 15 | internal extension Unboxable { 16 | static func makeTransform() -> UnboxTransform { 17 | return { try ($0 as? UnboxableDictionary).map(unbox) } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MBProgressHUD/MBProgressHUD.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/MBProgressHUD 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" 4 | OTHER_LDFLAGS = -framework "CoreGraphics" -framework "QuartzCore" 5 | PODS_BUILD_DIR = $BUILD_DIR 6 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/MBProgressHUD 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyUserDefaults/SwiftyUserDefaults.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/SwiftyUserDefaults 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" 4 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 5 | PODS_BUILD_DIR = $BUILD_DIR 6 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftyUserDefaults 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /MoviesApp/Coordinator/Router.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class Router { 4 | 5 | private weak var rootController: UINavigationController? 6 | 7 | init(with rootController: UINavigationController) { 8 | self.rootController = rootController 9 | } 10 | 11 | func setRootModule(_ module: Presentable) { 12 | guard let controller = module.toPresent() else { return } 13 | rootController?.setViewControllers([controller], animated: false) 14 | } 15 | 16 | func push(_ module: Presentable) { 17 | guard let controller = module.toPresent() else { return } 18 | rootController?.pushViewController(controller, animated: true) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Unbox/Unbox.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/Unbox 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" 4 | OTHER_LDFLAGS = -framework "CoreGraphics" -framework "Foundation" 5 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 6 | PODS_BUILD_DIR = $BUILD_DIR 7 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_ROOT = ${SRCROOT} 9 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/Unbox 10 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 11 | SKIP_INSTALL = YES 12 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Optional+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension Optional { 10 | func map(_ transform: (Wrapped) throws -> T?) rethrows -> T? { 11 | guard let value = self else { 12 | return nil 13 | } 14 | 15 | return try transform(value) 16 | } 17 | 18 | func orThrow(_ errorClosure: @autoclosure () -> E) throws -> Wrapped { 19 | guard let value = self else { 20 | throw errorClosure() 21 | } 22 | 23 | return value 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (4.5.1) 3 | - MBProgressHUD (1.0.0) 4 | - SwiftyUserDefaults (3.0.0) 5 | - TableKit (2.5.0) 6 | - Unbox (2.5.0) 7 | 8 | DEPENDENCIES: 9 | - Alamofire 10 | - MBProgressHUD 11 | - SwiftyUserDefaults 12 | - TableKit 13 | - Unbox 14 | 15 | SPEC CHECKSUMS: 16 | Alamofire: 2d95912bf4c34f164fdfc335872e8c312acaea4a 17 | MBProgressHUD: 4890f671c94e8a0f3cf959aa731e9de2f036d71a 18 | SwiftyUserDefaults: 0f1d45fc3aafb9064dac661e367f8f83fe21a4b4 19 | TableKit: 42d4dff2944f273cdeec2ef6352064eb6a9a355b 20 | Unbox: 30e437e6151a6de16139375fd4e8dd9a664cfbf7 21 | 22 | PODFILE CHECKSUM: 71ca4b30e7c2fa2c6d82eefe845d680b018ad254 23 | 24 | COCOAPODS: 1.2.0 25 | -------------------------------------------------------------------------------- /Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (4.5.1) 3 | - MBProgressHUD (1.0.0) 4 | - SwiftyUserDefaults (3.0.0) 5 | - TableKit (2.5.0) 6 | - Unbox (2.5.0) 7 | 8 | DEPENDENCIES: 9 | - Alamofire 10 | - MBProgressHUD 11 | - SwiftyUserDefaults 12 | - TableKit 13 | - Unbox 14 | 15 | SPEC CHECKSUMS: 16 | Alamofire: 2d95912bf4c34f164fdfc335872e8c312acaea4a 17 | MBProgressHUD: 4890f671c94e8a0f3cf959aa731e9de2f036d71a 18 | SwiftyUserDefaults: 0f1d45fc3aafb9064dac661e367f8f83fe21a4b4 19 | TableKit: 42d4dff2944f273cdeec2ef6352064eb6a9a355b 20 | Unbox: 30e437e6151a6de16139375fd4e8dd9a664cfbf7 21 | 22 | PODFILE CHECKSUM: 71ca4b30e7c2fa2c6d82eefe845d680b018ad254 23 | 24 | COCOAPODS: 1.2.0 25 | -------------------------------------------------------------------------------- /MoviesApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | private var appCoordinator: Coordinator! 8 | 9 | func application(_ application: UIApplication, 10 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 11 | let navController = UINavigationController() 12 | appCoordinator = AppCoordinator(router: Router(with: navController)) 13 | window = UIWindow(frame: UIScreen.main.bounds) 14 | window?.rootViewController = navController 15 | window?.makeKeyAndVisible() 16 | 17 | appCoordinator.start() 18 | 19 | return true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Set+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `Set` an unboxable collection 10 | extension Set: UnboxableCollection { 11 | public typealias UnboxValue = Element 12 | 13 | public static func unbox(value: Any, allowInvalidElements: Bool, transformer: T) throws -> Set? where T.UnboxedElement == UnboxValue { 14 | guard let array = try [UnboxValue].unbox(value: value, allowInvalidElements: allowInvalidElements, transformer: transformer) else { 15 | return nil 16 | } 17 | 18 | return Set(array) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Bool+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `Bool` an Unboxable raw type 10 | extension Bool: UnboxableRawType { 11 | public static func transform(unboxedNumber: NSNumber) -> Bool? { 12 | return unboxedNumber.boolValue 13 | } 14 | 15 | public static func transform(unboxedString: String) -> Bool? { 16 | switch unboxedString.lowercased() { 17 | case "true", "t", "y", "yes": 18 | return true 19 | case "false", "f" , "n", "no": 20 | return false 21 | default: 22 | return nil 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxArrayContainer.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | internal struct UnboxArrayContainer: UnboxableWithContext { 10 | let models: [T] 11 | 12 | init(unboxer: Unboxer, context: (path: UnboxPath, allowInvalidElements: Bool)) throws { 13 | switch context.path { 14 | case .key(let key): 15 | self.models = try unboxer.unbox(key: key, allowInvalidElements: context.allowInvalidElements) 16 | case .keyPath(let keyPath): 17 | self.models = try unboxer.unbox(keyPath: keyPath, allowInvalidElements: context.allowInvalidElements) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MoviesAppTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | -------------------------------------------------------------------------------- /MoviesApp/Features/MovieList/MovieListViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import TableKit 3 | 4 | class MovieListViewController: ViewController, MovieListView { 5 | 6 | var presenter: MovieListPresenter! 7 | 8 | override func viewDidLoad() { 9 | super.viewDidLoad() 10 | title = "movie_list_screen_title".localized 11 | setRefreshControlOn(tableView, action: #selector(MovieListViewController.refresh)) 12 | presenter.onViewDidLoad() 13 | } 14 | 15 | func show(movies: [MovieViewModel]) { 16 | tableDirector.clear() 17 | let rows: [Row] = movies.map { TableRow(item: $0) } 18 | tableDirector += TableSection(rows: rows) 19 | tableView.reloadData() 20 | } 21 | 22 | @objc func refresh() { 23 | presenter.onRefresh() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MoviesAppTests/Slate/Equivalent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import MoviesApp 3 | 4 | enum Equivalent: Equatable { 5 | case int(Int?) 6 | case string(String?) 7 | case strings([String]) 8 | case date(Date?) 9 | case movies([MovieViewModel]) 10 | } 11 | 12 | func == (lhs: Equivalent, rhs: Equivalent) -> Bool { 13 | switch (lhs, rhs) { 14 | case (let .int(value1), let .int(value2)): return value1 == value2 15 | case (let .string(value1), let .string(value2)): return value1 == value2 16 | case (let .strings(value1), let .strings(value2)): return value1 == value2 17 | case (let .date(value1), let .date(value2)): return value1 == value2 18 | case (let .movies(value1), let .movies(value2)): return value1 == value2 19 | default: return false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MoviesApp/Protocols/Refreshable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol Refreshable: class { 4 | var refreshControl: UIRefreshControl? { get set } 5 | func beginRefreshing() 6 | func endRefreshing() 7 | } 8 | 9 | extension Refreshable { 10 | 11 | func setRefreshControlOn(_ tableView: UITableView, action: Selector) { 12 | guard self.refreshControl == nil else { return } 13 | 14 | let refreshControl = UIRefreshControl() 15 | refreshControl.addTarget(self, action: action, for: .valueChanged) 16 | tableView.refreshControl = refreshControl 17 | self.refreshControl = refreshControl 18 | } 19 | 20 | func beginRefreshing() { 21 | refreshControl?.beginRefreshing() 22 | } 23 | 24 | func endRefreshing() { 25 | refreshControl?.endRefreshing() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MoviesApp/Coordinator/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | class AppCoordinator: Coordinator { 2 | 3 | private let router: Router 4 | private let factory: Factory 5 | 6 | init(router: Router, factory: Factory = .init()) { 7 | self.router = router 8 | self.factory = factory 9 | } 10 | 11 | func start() { 12 | showSearchModule() 13 | } 14 | 15 | private func showSearchModule() { 16 | let searchModule = factory.makeSearchModule() 17 | searchModule.onMoviesSelected = { [weak self] movies in 18 | self?.showMovieList(with: movies) 19 | } 20 | router.setRootModule(searchModule) 21 | } 22 | 23 | private func showMovieList(with movies: [Movie]) { 24 | let movieListModule = factory.makeMovieListModule(with: movies) 25 | router.push(movieListModule) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxCollectionElementTransformer.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used to unbox an element in a collection. Unbox provides default implementations of this protocol. 10 | public protocol UnboxCollectionElementTransformer { 11 | /// The raw element type that this transformer expects as input 12 | associatedtype UnboxRawElement 13 | /// The unboxed element type that this transformer outputs 14 | associatedtype UnboxedElement 15 | 16 | /// Unbox an element from a collection, optionally allowing invalid elements for nested collections 17 | func unbox(element: UnboxRawElement, allowInvalidCollectionElements: Bool) throws -> UnboxedElement? 18 | } 19 | -------------------------------------------------------------------------------- /MoviesApp/Coordinator/Factory.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class Factory { 4 | 5 | func makeSearchModule() -> SearchView { 6 | let storyboard = UIStoryboard(name: "SearchScreen", bundle: nil) 7 | let controller = storyboard.instantiateViewController(withIdentifier: "SearchViewController") as! SearchViewController 8 | controller.presenter = SearchPresenter(view: controller) 9 | return controller 10 | } 11 | 12 | func makeMovieListModule(with movies: [Movie]) -> MovieListView { 13 | let storyboard = UIStoryboard(name: "MovieListScreen", bundle: nil) 14 | let controller = storyboard.instantiateViewController(withIdentifier: "MovieListViewController") as! MovieListViewController 15 | controller.presenter = MovieListPresenter(view: controller, movies: movies) 16 | return controller 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MoviesApp/Features/Search/Storage.swift: -------------------------------------------------------------------------------- 1 | import SwiftyUserDefaults 2 | 3 | class Storage { 4 | 5 | private var suggestions: [String] 6 | 7 | init() { 8 | self.suggestions = Defaults[.suggestions] ?? [] 9 | } 10 | 11 | func add(suggestion: String) { 12 | guard suggestions.first(where: { $0 == suggestion }) == nil else { return } 13 | 14 | if suggestions.count == 10 { 15 | suggestions.remove(at: 0) 16 | } 17 | suggestions.append(suggestion) 18 | Defaults[.suggestions] = suggestions 19 | } 20 | 21 | func getSuggestions() -> [String] { 22 | return suggestions 23 | } 24 | 25 | func getLastSuggestion() -> String? { 26 | return suggestions.last 27 | } 28 | } 29 | 30 | extension DefaultsKeys { 31 | static let suggestions = DefaultsKey<[String]?>("suggestions") 32 | } 33 | -------------------------------------------------------------------------------- /MoviesApp/Features/MovieList/MovieListCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import TableKit 3 | 4 | class MovieListCell: UITableViewCell, ConfigurableCell { 5 | 6 | @IBOutlet weak var posterImageView: UIImageView! 7 | @IBOutlet weak var titleLabel: UILabel! 8 | @IBOutlet weak var descriptionLabel: UILabel! 9 | private weak var viewModel: MovieViewModel? 10 | 11 | func configure(with movie: MovieViewModel) { 12 | titleLabel.text = movie.title 13 | descriptionLabel.text = movie.overview 14 | viewModel = movie 15 | 16 | viewModel?.loadImage { [weak self] image in 17 | guard let image = image else { return } 18 | UIView.animate(withDuration: 0.3) { 19 | self?.posterImageView.image = image 20 | } 21 | } 22 | } 23 | 24 | override func prepareForReuse() { 25 | super.prepareForReuse() 26 | viewModel = nil 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MoviesAppTests/Features/MovieList/MovieListRepositoryMock.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MoviesApp 3 | 4 | class MovieListRepositoryMock: MovieListRepository, Mock { 5 | 6 | let callHandler: CallHandler 7 | var state: RepositoryMockState<[Movie]> = .success 8 | 9 | init(with testCase: BaseTestCase) { 10 | callHandler = CallHandler(withTestCase: testCase) 11 | } 12 | 13 | func instanceType() -> MovieListRepositoryMock { 14 | return self 15 | } 16 | 17 | override func repeatLastSearch(onSuccess: @escaping ([Movie]) -> Void, onError: @escaping (String) -> Void) { 18 | callHandler.accept(function: #function, file: #file, line: #line) 19 | 20 | switch state { 21 | case .success: onSuccess(MovieBuilder.movies()) 22 | case .failWith(let string): onError(string) 23 | default: onError("error") 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxableByTransform.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used to enable any type as being unboxable, by transforming a raw value 10 | public protocol UnboxableByTransform: UnboxCompatible { 11 | /// The type of raw value that this type can be transformed from. Must be a valid JSON type. 12 | associatedtype UnboxRawValue 13 | 14 | /// Attempt to transform a raw unboxed value into an instance of this type 15 | static func transform(unboxedValue: UnboxRawValue) -> Self? 16 | } 17 | 18 | /// Default implementation of `UnboxCompatible` for transformable types 19 | public extension UnboxableByTransform { 20 | static func unbox(value: Any, allowInvalidCollectionElements: Bool) throws -> Self? { 21 | return (value as? UnboxRawValue).map(self.transform) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Unbox/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 2.5.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Alamofire/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 4.5.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/TableKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 2.5.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MBProgressHUD/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MoviesApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyUserDefaults/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 3.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /MoviesApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | } 43 | ], 44 | "info" : { 45 | "version" : 1, 46 | "author" : "xcode" 47 | } 48 | } -------------------------------------------------------------------------------- /MoviesApp/Features/MovieList/MovieListPresenter.swift: -------------------------------------------------------------------------------- 1 | class MovieListPresenter { 2 | 3 | private weak var view: MovieListView? 4 | private var movieViewModels: [MovieViewModel] 5 | private let repository: MovieListRepository 6 | 7 | init(view: MovieListView, movies: [Movie], repository: MovieListRepository = .init()) { 8 | self.view = view 9 | self.movieViewModels = movies.map { MovieViewModel(with: $0) } 10 | self.repository = repository 11 | } 12 | 13 | func onViewDidLoad() { 14 | view?.show(movies: movieViewModels) 15 | } 16 | 17 | func onRefresh() { 18 | repository.repeatLastSearch(onSuccess: { [weak self] movies in 19 | guard let `self` = self else { return } 20 | 21 | self.movieViewModels = movies.map { MovieViewModel(with: $0) } 22 | self.view?.endRefreshing() 23 | self.view?.show(movies: self.movieViewModels) 24 | 25 | }, onError: { [weak self] message in 26 | self?.view?.endRefreshing() 27 | self?.view?.show(message: message) 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MoviesAppTests/Test environment/Builders/MovieBuilder.swift: -------------------------------------------------------------------------------- 1 | @testable import MoviesApp 2 | 3 | class MovieBuilder { 4 | 5 | static func movie() -> Movie { 6 | return Movie(title: "test1", releaseDate: "05-03-1987", overview: "testtest", imagePath: "/wgwe.jpg") 7 | } 8 | 9 | static func movies() -> [Movie] { 10 | return [ 11 | Movie(title: "test1", releaseDate: "05-03-1987", overview: "testtest1", imagePath: "/wgwe.jpg"), 12 | Movie(title: "test2", releaseDate: "05-03-1988", overview: "testtest2", imagePath: "/wgqwe.jpg"), 13 | Movie(title: "test3", releaseDate: "05-03-1989", overview: "testtest3", imagePath: "/wwgwe.jpg"), 14 | Movie(title: "test4", releaseDate: "05-03-1990", overview: "testtest4", imagePath: "/wegwe.jpg") 15 | ] 16 | } 17 | } 18 | 19 | extension Movie { 20 | 21 | init(title: String, releaseDate: String?, overview: String?, imagePath: String?) { 22 | self.title = title 23 | self.releaseDate = releaseDate 24 | self.overview = overview 25 | self.imagePath = imagePath 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxCompatible.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol that types that can be used in an unboxing process must conform to. You don't conform to this protocol yourself. 10 | public protocol UnboxCompatible { 11 | /// Unbox a value, or either throw or return nil if unboxing couldn't be performed 12 | static func unbox(value: Any, allowInvalidCollectionElements: Bool) throws -> Self? 13 | } 14 | 15 | // MARK: - Internal extensions 16 | 17 | internal extension UnboxCompatible { 18 | static func unbox(value: Any) throws -> Self? { 19 | return try self.unbox(value: value, allowInvalidCollectionElements: false) 20 | } 21 | } 22 | 23 | internal extension UnboxCompatible where Self: Collection { 24 | static func makeTransform(allowInvalidElements: Bool) -> UnboxTransform { 25 | return { 26 | try self.unbox(value: $0, allowInvalidCollectionElements: allowInvalidElements) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Pods/MBProgressHUD/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2009-2016 Matej Bukovinski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /MoviesApp/Features/Search/SearchRepository.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | 3 | class SearchRepository { 4 | 5 | let storage: Storage 6 | 7 | init(storage: Storage = .init()) { 8 | self.storage = storage 9 | } 10 | 11 | func getLastSearchResults(onSuccess: @escaping ([String]) -> Void) { 12 | onSuccess(storage.getSuggestions()) 13 | } 14 | 15 | func searchMovies(with name: String, onSuccess: @escaping ([Movie]) -> Void, onError: @escaping (String) -> Void) { 16 | let requestString = URL.forRequest + name.trimmed 17 | Alamofire.request(requestString).responseJSON { [weak self] response in 18 | if let JSON = response.result.value as? [String: AnyObject], 19 | let jsonArray = JSON["results"] as? [[String: AnyObject]] { 20 | 21 | if let movies: [Movie] = Movie.deserialize(with: jsonArray), !movies.isEmpty { 22 | self?.storage.add(suggestion: name) 23 | onSuccess(movies) 24 | 25 | } else { 26 | onError("not_found".localized) 27 | } 28 | } else { 29 | onError("internet_fail".localized) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MoviesApp/Features/Search/SearchPresenter.swift: -------------------------------------------------------------------------------- 1 | class SearchPresenter { 2 | 3 | private weak var view: SearchView? 4 | private var repository: SearchRepository 5 | 6 | init(view: SearchView, repository: SearchRepository = .init()) { 7 | self.view = view 8 | self.repository = repository 9 | } 10 | 11 | func onViewWillAppear() { 12 | updateSuggestions() 13 | } 14 | 15 | func onSearch(_ text: String?) { 16 | guard let text = text else { return } 17 | 18 | view?.showLoadingIndicator() 19 | repository.searchMovies(with: text, onSuccess: { [weak self] movies in 20 | self?.view?.hideLoadingIndicator() 21 | self?.updateSuggestions() 22 | self?.view?.onMoviesSelected?(movies) 23 | 24 | }, onError: { [weak self] message in 25 | self?.view?.hideLoadingIndicator() 26 | self?.view?.show(message: message) 27 | }) 28 | } 29 | 30 | private func updateSuggestions() { 31 | repository.getLastSearchResults { [unowned self] suggestions in 32 | guard !suggestions.isEmpty else { return } 33 | self.view?.show(suggestions: suggestions) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Pods/TableKit/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Pods/Unbox/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 John Sundell 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 | 23 | -------------------------------------------------------------------------------- /Pods/Alamofire/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2017 Alamofire Software Foundation (http://alamofire.org/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Pods/SwiftyUserDefaults/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Radosław Pietruszewski 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 | -------------------------------------------------------------------------------- /MoviesAppTests/Features/Search/SearchRepositoryMock.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import MoviesApp 4 | 5 | class SearchRepositoryMock: SearchRepository, Mock { 6 | 7 | let callHandler: CallHandler 8 | var state: RepositoryMockState<[Movie]> = .success 9 | 10 | init(with testCase: BaseTestCase) { 11 | callHandler = CallHandler(withTestCase: testCase) 12 | } 13 | 14 | func instanceType() -> SearchRepositoryMock { 15 | return self 16 | } 17 | 18 | override func getLastSearchResults(onSuccess: @escaping ([String]) -> Void) { 19 | callHandler.accept(function: #function, file: #file, line: #line) 20 | 21 | switch state { 22 | case .success: onSuccess(SuggestionBuilder.suggestions()) 23 | default: onSuccess([]) 24 | } 25 | } 26 | 27 | override func searchMovies(with name: String, onSuccess: @escaping ([Movie]) -> Void, onError: @escaping (String) -> Void) { 28 | callHandler.accept(function: #function, file: #file, line: #line) 29 | 30 | switch state { 31 | case .success: onSuccess(MovieBuilder.movies()) 32 | case .failWith(let string): onError(string) 33 | default: onError("error") 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxableRawType.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used to enable a raw type (such as `Int` or `String`) for Unboxing 10 | public protocol UnboxableRawType: UnboxCompatible { 11 | /// Transform an instance of this type from an unboxed number 12 | static func transform(unboxedNumber: NSNumber) -> Self? 13 | /// Transform an instance of this type from an unboxed string 14 | static func transform(unboxedString: String) -> Self? 15 | } 16 | 17 | // Default implementation of `UnboxCompatible` for raw types 18 | public extension UnboxableRawType { 19 | static func unbox(value: Any, allowInvalidCollectionElements: Bool) throws -> Self? { 20 | if let matchedValue = value as? Self { 21 | return matchedValue 22 | } 23 | 24 | if let string = value as? String { 25 | return self.transform(unboxedString: string) 26 | } 27 | 28 | if let number = value as? NSNumber { 29 | return self.transform(unboxedNumber: number) 30 | } 31 | 32 | return nil 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MoviesAppTests/Features/MovieList/MovieListViewMock.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MoviesApp 3 | 4 | class MovieListViewMock: MovieListView, Mock { 5 | 6 | let callHandler: CallHandler 7 | var refreshControl: UIRefreshControl? 8 | 9 | init(with testCase: BaseTestCase) { 10 | callHandler = CallHandler(withTestCase: testCase) 11 | } 12 | 13 | func instanceType() -> MovieListViewMock { 14 | return self 15 | } 16 | 17 | func show(movies: [MovieViewModel]) { 18 | callHandler.accept(function: #function, file: #file, line: #line) 19 | .join(with: .movies(movies)) 20 | .check(function: #function, file: #file, line: #line) 21 | } 22 | 23 | func show(message text: String) { 24 | callHandler.accept(function: #function, file: #file, line: #line) 25 | .join(with: .string(text)) 26 | .check(function: #function, file: #file, line: #line) 27 | } 28 | 29 | func beginRefreshing() { 30 | callHandler.accept(function: #function, file: #file, line: #line) 31 | } 32 | 33 | func endRefreshing() { 34 | callHandler.accept(function: #function, file: #file, line: #line) 35 | } 36 | 37 | func toPresent() -> UIViewController? { 38 | return nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxError.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Error type that Unbox throws in case an unrecoverable error was encountered 10 | public enum UnboxError: Error { 11 | /// Invalid data was provided when calling unbox(data:...) 12 | case invalidData 13 | /// Custom unboxing failed, either by throwing or returning `nil` 14 | case customUnboxingFailed 15 | /// An error occurred while unboxing a value for a path (contains the underlying path error, and the path) 16 | case pathError(UnboxPathError, String) 17 | } 18 | 19 | /// Extension making `UnboxError` conform to `CustomStringConvertible` 20 | extension UnboxError: CustomStringConvertible { 21 | public var description: String { 22 | switch self { 23 | case .invalidData: 24 | return "[UnboxError] Invalid data." 25 | case .customUnboxingFailed: 26 | return "[UnboxError] Custom unboxing failed." 27 | case .pathError(let error, let path): 28 | return "[UnboxError] An error occurred while unboxing path \"\(path)\": \(error)" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MoviesAppTests/Features/Search/SearchViewMock.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MoviesApp 3 | 4 | class SearchViewMock: SearchView, Mock { 5 | 6 | let callHandler: CallHandler 7 | 8 | init(with testCase: BaseTestCase) { 9 | callHandler = CallHandler(withTestCase: testCase) 10 | } 11 | 12 | func instanceType() -> SearchViewMock { 13 | return self 14 | } 15 | 16 | var onMoviesSelected: (([Movie]) -> Void)? 17 | 18 | func show(suggestions: [String]) { 19 | callHandler 20 | .accept(function: #function, file: #file, line: #line) 21 | .join(with: .strings(suggestions)) 22 | .check(function: #function, file: #file, line: #line) 23 | } 24 | 25 | func showLoadingIndicator() { 26 | callHandler.accept(function: #function, file: #file, line: #line) 27 | } 28 | 29 | func hideLoadingIndicator() { 30 | callHandler.accept(function: #function, file: #file, line: #line) 31 | } 32 | 33 | func show(message text: String) { 34 | callHandler 35 | .accept(function: #function, file: #file, line: #line) 36 | .join(with: .string(text)) 37 | .check(function: #function, file: #file, line: #line) 38 | } 39 | 40 | func toPresent() -> UIViewController? { 41 | return nil 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MoviesApp/Pods-MoviesApp.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/Alamofire" "$PODS_CONFIGURATION_BUILD_DIR/MBProgressHUD" "$PODS_CONFIGURATION_BUILD_DIR/SwiftyUserDefaults" "$PODS_CONFIGURATION_BUILD_DIR/TableKit" "$PODS_CONFIGURATION_BUILD_DIR/Unbox" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/Alamofire/Alamofire.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/MBProgressHUD/MBProgressHUD.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SwiftyUserDefaults/SwiftyUserDefaults.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/TableKit/TableKit.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/Unbox/Unbox.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Alamofire" -framework "MBProgressHUD" -framework "SwiftyUserDefaults" -framework "TableKit" -framework "Unbox" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = $BUILD_DIR 9 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT}/Pods 11 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MoviesApp/Pods-MoviesApp.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/Alamofire" "$PODS_CONFIGURATION_BUILD_DIR/MBProgressHUD" "$PODS_CONFIGURATION_BUILD_DIR/SwiftyUserDefaults" "$PODS_CONFIGURATION_BUILD_DIR/TableKit" "$PODS_CONFIGURATION_BUILD_DIR/Unbox" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/Alamofire/Alamofire.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/MBProgressHUD/MBProgressHUD.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SwiftyUserDefaults/SwiftyUserDefaults.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/TableKit/TableKit.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/Unbox/Unbox.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Alamofire" -framework "MBProgressHUD" -framework "SwiftyUserDefaults" -framework "TableKit" -framework "Unbox" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = $BUILD_DIR 9 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT}/Pods 11 | -------------------------------------------------------------------------------- /MoviesAppTests/Slate/BaseTestCase.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | typealias Function = String 4 | 5 | class BaseTestCase: XCTestCase { 6 | 7 | var callHistory: [Function]! 8 | var callMockHistory: [Function]! 9 | 10 | override open func setUp() { 11 | super.setUp() 12 | 13 | callHistory = [] 14 | callMockHistory = [] 15 | } 16 | 17 | override open func tearDown() { 18 | callHistory = nil 19 | callMockHistory = nil 20 | 21 | super.tearDown() 22 | } 23 | 24 | func verifyOrder(file: String = #file, line: UInt = #line) { 25 | guard callHistory.count == callMockHistory.count else { 26 | doFail( 27 | "Number of called functions are not equal: \(callHistory.count) != \(callMockHistory.count)", 28 | file: file, 29 | line: line 30 | ) 31 | return 32 | } 33 | 34 | for (index, history) in callHistory.enumerated() { 35 | if history != callMockHistory[index] { 36 | doFail( 37 | "Called function \(history) are not \(callMockHistory[index]) from the test", 38 | file: file, 39 | line: line 40 | ) 41 | } 42 | } 43 | } 44 | 45 | private func doFail(_ message: String, file: String, line: UInt) { 46 | recordFailure(withDescription: message, inFile: file, atLine: Int(line), expected: true) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Array+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `Array` an unboxable collection 10 | extension Array: UnboxableCollection { 11 | public typealias UnboxValue = Element 12 | 13 | public static func unbox(value: Any, allowInvalidElements: Bool, transformer: T) throws -> Array? where T.UnboxedElement == UnboxValue { 14 | guard let array = value as? [T.UnboxRawElement] else { 15 | return nil 16 | } 17 | 18 | return try array.enumerated().map(allowInvalidElements: allowInvalidElements) { index, element in 19 | let unboxedElement = try transformer.unbox(element: element, allowInvalidCollectionElements: allowInvalidElements) 20 | return try unboxedElement.orThrow(UnboxPathError.invalidArrayElement(element, index)) 21 | } 22 | } 23 | } 24 | 25 | /// Extension making `Array` an unbox path node 26 | extension Array: UnboxPathNode { 27 | func unboxPathValue(forKey key: String) -> Any? { 28 | guard let index = Int(key) else { 29 | return nil 30 | } 31 | 32 | if index >= self.count { 33 | return nil 34 | } 35 | 36 | return self[index] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MoviesApp/Features/Search/SearchViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import TableKit 3 | 4 | class SearchViewController: ViewController, SearchView, UISearchBarDelegate { 5 | 6 | @IBOutlet weak var searchBar: UISearchBar! { 7 | didSet { searchBar.placeholder = "search_placeholder".localized } 8 | } 9 | var onMoviesSelected: (([Movie]) -> Void)? 10 | var presenter: SearchPresenter! 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | title = "search_screen_title".localized 15 | } 16 | 17 | override func viewWillAppear(_ animated: Bool) { 18 | super.viewWillAppear(animated) 19 | presenter.onViewWillAppear() 20 | searchBar.becomeFirstResponder() 21 | } 22 | 23 | override func viewWillDisappear(_ animated: Bool) { 24 | super.viewWillDisappear(animated) 25 | searchBar.resignFirstResponder() 26 | } 27 | 28 | func show(suggestions: [String]) { 29 | tableDirector.clear() 30 | let action = TableRowAction(.click) { [unowned self] options in 31 | self.presenter.onSearch(options.item) 32 | } 33 | let rows: [Row] = suggestions.map { TableRow(item: $0, actions: [action]) } 34 | tableDirector += TableSection(headerTitle: "search_sceen_header".localized, footerTitle: nil, rows: rows) 35 | 36 | tableView.reloadData() 37 | } 38 | 39 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 40 | presenter.onSearch(searchBar.text) 41 | searchBar.resignFirstResponder() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Data+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension Data { 10 | func unbox() throws -> T { 11 | return try Unboxer(data: self).performUnboxing() 12 | } 13 | 14 | func unbox(context: T.UnboxContext) throws -> T { 15 | return try Unboxer(data: self).performUnboxing(context: context) 16 | } 17 | 18 | func unbox(closure: (Unboxer) throws -> T?) throws -> T { 19 | return try closure(Unboxer(data: self)).orThrow(UnboxError.customUnboxingFailed) 20 | } 21 | 22 | func unbox(allowInvalidElements: Bool) throws -> [T] { 23 | let array: [UnboxableDictionary] = try JSONSerialization.unbox(data: self, options: [.allowFragments]) 24 | return try array.map(allowInvalidElements: allowInvalidElements) { dictionary in 25 | return try Unboxer(dictionary: dictionary).performUnboxing() 26 | } 27 | } 28 | 29 | func unbox(context: T.UnboxContext, allowInvalidElements: Bool) throws -> [T] { 30 | let array: [UnboxableDictionary] = try JSONSerialization.unbox(data: self, options: [.allowFragments]) 31 | 32 | return try array.map(allowInvalidElements: allowInvalidElements) { dictionary in 33 | return try Unboxer(dictionary: dictionary).performUnboxing(context: context) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MoviesApp/Features/MovieList/MovieViewModel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Alamofire 3 | 4 | class MovieViewModel: Equatable { 5 | 6 | var title: String { 7 | if let date = date { 8 | return movie.title + ", " + "(" + date + ")" 9 | } 10 | return movie.title 11 | } 12 | var overview: String { 13 | return movie.overview ?? "not set" 14 | } 15 | var date: String? { 16 | let formatter = DateFormatter() 17 | formatter.dateFormat = "yyyy-MM-dd" 18 | guard let date = formatter.date(from: movie.releaseDate ?? "") else { return nil } 19 | let components = Calendar.current.dateComponents([.year], from: date) 20 | if let year = components.year { 21 | return "\(year)" 22 | } 23 | return nil 24 | } 25 | var image: UIImage? 26 | 27 | private let movie: Movie 28 | 29 | init(with movie: Movie) { 30 | self.movie = movie 31 | } 32 | 33 | func loadImage(onSuccess: @escaping (UIImage?) -> Void) { 34 | guard image == nil else { onSuccess(image); return } 35 | guard let path = movie.imagePath else { return } 36 | 37 | let request = Alamofire.request(URL.forImage + path) 38 | request.responseData { [weak self] response in 39 | guard let data = response.result.value else { return } 40 | self?.image = UIImage(data: data) 41 | onSuccess(self?.image) 42 | } 43 | } 44 | } 45 | 46 | func == (lhs: MovieViewModel, rhs: MovieViewModel) -> Bool { 47 | return lhs.title == rhs.title 48 | && lhs.overview == rhs.overview 49 | && lhs.date == rhs.date 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /MoviesAppTests/Features/MovieList/MovieListPresenterTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import MoviesApp 4 | 5 | class MovieListPresenterTest: BaseTestCase { 6 | 7 | var presenter: MovieListPresenter! 8 | var view: MovieListViewMock! 9 | var repository: MovieListRepositoryMock! 10 | 11 | override func setUp() { 12 | super.setUp() 13 | 14 | view = MovieListViewMock(with: self) 15 | repository = MovieListRepositoryMock(with: self) 16 | presenter = MovieListPresenter(view: view, movies: MovieBuilder.movies(), repository: repository) 17 | } 18 | 19 | override func tearDown() { 20 | 21 | view = nil 22 | repository = nil 23 | presenter = nil 24 | super.tearDown() 25 | } 26 | 27 | func testOnViewDidLoad() { 28 | let viewModels = MovieBuilder.movies().map { MovieViewModel(with: $0) } 29 | 30 | presenter.onViewDidLoad() 31 | 32 | verify(view).show(movies: viewModels) 33 | verifyOrder() 34 | } 35 | 36 | func testOnRefreshWithSuccess() { 37 | let viewModels = MovieBuilder.movies().map { MovieViewModel(with: $0) } 38 | repository.state = .success 39 | 40 | presenter.onRefresh() 41 | 42 | verify(repository).repeatLastSearch(onSuccess: { _ in }, onError: { _ in }) 43 | verify(view).endRefreshing() 44 | verify(view).show(movies: viewModels) 45 | verifyOrder() 46 | } 47 | 48 | func testOnRefreshWithFail() { 49 | repository.state = .failure 50 | 51 | presenter.onRefresh() 52 | 53 | verify(repository).repeatLastSearch(onSuccess: { _ in }, onError: { _ in }) 54 | verify(view).endRefreshing() 55 | verify(view).show(message: "error") 56 | verifyOrder() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MoviesApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | NSAppTransportSecurity 36 | 37 | NSExceptionDomains 38 | 39 | themoviedb.org 40 | 41 | NSExceptionAllowsInsecureHTTPLoads 42 | 43 | NSIncludesSubdomains 44 | 45 | 46 | tmdb.org 47 | 48 | NSExceptionAllowsInsecureHTTPLoads 49 | 50 | NSIncludesSubdomains 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /MoviesApp.xcworkspace/xcshareddata/MoviesApp.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "9D9BD237407B565E3BAEB13468D544D632D1CAE1", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "4411F98DCFE93E1498D3F810B2AE12CC6905BB74" : 9223372036854775807, 8 | "9D9BD237407B565E3BAEB13468D544D632D1CAE1" : 9223372036854775807 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "339F9D1C-EF01-4452-BCAA-92D621031CE7", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "4411F98DCFE93E1498D3F810B2AE12CC6905BB74" : "cassandra\/", 13 | "9D9BD237407B565E3BAEB13468D544D632D1CAE1" : "MoviesApp\/" 14 | }, 15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "MoviesApp", 16 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "MoviesApp.xcworkspace", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 19 | { 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/blacklane\/cassandra", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "4411F98DCFE93E1498D3F810B2AE12CC6905BB74" 23 | }, 24 | { 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/AndreyPanov\/MoviesApp", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "9D9BD237407B565E3BAEB13468D544D632D1CAE1" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxFormatter.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used by objects that may format raw values into some other value 10 | public protocol UnboxFormatter { 11 | /// The type of raw value that this formatter accepts as input 12 | associatedtype UnboxRawValue: UnboxableRawType 13 | /// The type of value that this formatter produces as output 14 | associatedtype UnboxFormattedType 15 | 16 | /// Format an unboxed value into another value (or nil if the formatting failed) 17 | func format(unboxedValue: UnboxRawValue) -> UnboxFormattedType? 18 | } 19 | 20 | // MARK: - Internal extensions 21 | 22 | internal extension UnboxFormatter { 23 | func makeTransform() -> UnboxTransform { 24 | return { ($0 as? UnboxRawValue).map(self.format) } 25 | } 26 | 27 | func makeCollectionTransform(allowInvalidElements: Bool) -> UnboxTransform where C.UnboxValue == UnboxFormattedType { 28 | return { 29 | let transformer = UnboxFormatterCollectionElementTransformer(formatter: self) 30 | return try C.unbox(value: $0, allowInvalidElements: allowInvalidElements, transformer: transformer) 31 | } 32 | } 33 | } 34 | 35 | // MARK: - Utilities 36 | 37 | private class UnboxFormatterCollectionElementTransformer: UnboxCollectionElementTransformer { 38 | private let formatter: T 39 | 40 | init(formatter: T) { 41 | self.formatter = formatter 42 | } 43 | 44 | func unbox(element: T.UnboxRawValue, allowInvalidCollectionElements: Bool) throws -> T.UnboxFormattedType? { 45 | return self.formatter.format(unboxedValue: element) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MoviesApp 2 | Example application shows production architecture approach. 3 | Navigation logic driven by the `Coordinator` [My explanation](https://medium.com/blacklane-engineering/coordinators-essential-tutorial-part-i-376c836e9ba7). Coordinator uses `Factory` for modules' building and `Router` for 'push' action. 4 | In this example we have 2 modules for search and and for MovieList. Every module consists of `Presenter`(orchestrate business logic), `Repository` (responsible for storage and communication with server) and `ViewController` (responsible for user interaction, passive, just notify presenter what's going on). 5 | `Presenters` cover by unit test. 6 | `ViewModel` use for prepare data for cell's representation and load images. 7 | 8 | ##Update: Added Mock classes for functions' order check and parameters' check 9 | 10 | ##Pods 11 | - [TableKit](https://github.com/maxsokolov/TableKit) TableKit is a super lightweight yet powerful generic library that allows you to build complex table views in a declarative type-safe manner. 12 | - [Alamofire](https://github.com/Alamofire/Alamofire) HTTP networking library written in Swift. 13 | - [SwiftyUserDefaults](https://github.com/radex/SwiftyUserDefaults) SwiftyUserDefaults makes user defaults enjoyable to use by combining expressive Swifty API with the benefits of static typing. Define your keys in one place, use value types easily, and get extra safety and convenient compile-time checks for free. 14 | - [Unbox](https://github.com/JohnSundell/Unbox) Unbox is an easy to use Swift JSON decoder. 15 | - [MBProgressHUD](https://github.com/jdg/MBProgressHUD) iOS drop-in class that displays a translucent HUD with an indicator and/or labels while work is being done in a background thread. The HUD is meant as a replacement for the undocumented, private UIKit UIProgressHUD with some additional features. 16 | -------------------------------------------------------------------------------- /Pods/TableKit/Sources/ConfigurableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | // this software and associated documentation files (the "Software"), to deal in 6 | // the Software without restriction, including without limitation the rights to 7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | // the Software, and to permit persons to whom the Software is furnished to do so, 9 | // subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import UIKit 22 | 23 | public protocol ConfigurableCell { 24 | 25 | associatedtype T 26 | 27 | static var reuseIdentifier: String { get } 28 | static var estimatedHeight: CGFloat? { get } 29 | static var defaultHeight: CGFloat? { get } 30 | 31 | func configure(with _: T) 32 | } 33 | 34 | public extension ConfigurableCell where Self: UITableViewCell { 35 | 36 | static var reuseIdentifier: String { 37 | return String(describing: self) 38 | } 39 | 40 | static var estimatedHeight: CGFloat? { 41 | return nil 42 | } 43 | 44 | static var defaultHeight: CGFloat? { 45 | return nil 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Pods/Alamofire/Source/DispatchQueue+Alamofire.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueue+Alamofire.swift 3 | // 4 | // Copyright (c) 2014-2017 Alamofire Software Foundation (http://alamofire.org/) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Dispatch 26 | import Foundation 27 | 28 | extension DispatchQueue { 29 | static var userInteractive: DispatchQueue { return DispatchQueue.global(qos: .userInteractive) } 30 | static var userInitiated: DispatchQueue { return DispatchQueue.global(qos: .userInitiated) } 31 | static var utility: DispatchQueue { return DispatchQueue.global(qos: .utility) } 32 | static var background: DispatchQueue { return DispatchQueue.global(qos: .background) } 33 | 34 | func after(_ delay: TimeInterval, execute closure: @escaping () -> Void) { 35 | asyncAfter(deadline: .now() + delay, execute: closure) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Pods/TableKit/Sources/Operators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | // this software and associated documentation files (the "Software"), to deal in 6 | // the Software without restriction, including without limitation the rights to 7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | // the Software, and to permit persons to whom the Software is furnished to do so, 9 | // subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | // -- 22 | public func +=(left: TableDirector, right: TableSection) { 23 | left.append(section: right) 24 | } 25 | 26 | public func +=(left: TableDirector, right: [TableSection]) { 27 | left.append(sections: right) 28 | } 29 | 30 | // -- 31 | public func +=(left: TableDirector, right: Row) { 32 | left.append(sections: [TableSection(rows: [right])]) 33 | } 34 | 35 | public func +=(left: TableDirector, right: [Row]) { 36 | left.append(sections: [TableSection(rows: right)]) 37 | } 38 | 39 | // -- 40 | public func +=(left: TableSection, right: Row) { 41 | left.append(row: right) 42 | } 43 | 44 | public func +=(left: TableSection, right: [Row]) { 45 | left.append(rows: right) 46 | } 47 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxableWithContext.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used to declare a model as being Unboxable with a certain context, for use with the unbox(context:) function 10 | public protocol UnboxableWithContext { 11 | /// The type of the contextual object that this model requires when unboxed 12 | associatedtype UnboxContext 13 | 14 | /// Initialize an instance of this model by unboxing a dictionary & using a context 15 | init(unboxer: Unboxer, context: UnboxContext) throws 16 | } 17 | 18 | // MARK: - Internal extensions 19 | 20 | internal extension UnboxableWithContext { 21 | static func makeTransform(context: UnboxContext) -> UnboxTransform { 22 | return { 23 | try ($0 as? UnboxableDictionary).map { 24 | try unbox(dictionary: $0, context: context) 25 | } 26 | } 27 | } 28 | 29 | static func makeCollectionTransform(context: UnboxContext, allowInvalidElements: Bool) -> UnboxTransform where C.UnboxValue == Self { 30 | return { 31 | let transformer = UnboxableWithContextCollectionElementTransformer(context: context) 32 | return try C.unbox(value: $0, allowInvalidElements: allowInvalidElements, transformer: transformer) 33 | } 34 | } 35 | } 36 | 37 | // MARK: - Utilities 38 | 39 | private class UnboxableWithContextCollectionElementTransformer: UnboxCollectionElementTransformer { 40 | private let context: T.UnboxContext 41 | 42 | init(context: T.UnboxContext) { 43 | self.context = context 44 | } 45 | 46 | func unbox(element: UnboxableDictionary, allowInvalidCollectionElements: Bool) throws -> T? { 47 | let unboxer = Unboxer(dictionary: element) 48 | return try T(unboxer: unboxer, context: self.context) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Pods/TableKit/Sources/TableCellAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | // this software and associated documentation files (the "Software"), to deal in 6 | // the Software without restriction, including without limitation the rights to 7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | // the Software, and to permit persons to whom the Software is furnished to do so, 9 | // subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import UIKit 22 | 23 | /** 24 | A custom action that you can trigger from your cell. 25 | You can easily catch actions using a chaining manner with your row. 26 | */ 27 | open class TableCellAction { 28 | 29 | /// The cell that triggers an action. 30 | open let cell: UITableViewCell 31 | 32 | /// The action unique key. 33 | open let key: String 34 | 35 | /// The custom user info. 36 | open let userInfo: [AnyHashable: Any]? 37 | 38 | public init(key: String, sender: UITableViewCell, userInfo: [AnyHashable: Any]? = nil) { 39 | 40 | self.key = key 41 | self.cell = sender 42 | self.userInfo = userInfo 43 | } 44 | 45 | open func invoke() { 46 | NotificationCenter.default.post(name: Notification.Name(rawValue: TableKitNotifications.CellAction), object: self, userInfo: userInfo) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxPathError.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Type for errors that can occur while unboxing a certain path 10 | public enum UnboxPathError: Error { 11 | /// An empty key path was given 12 | case emptyKeyPath 13 | /// A required key was missing (contains the key) 14 | case missingKey(String) 15 | /// An invalid value was found (contains the value, and its key) 16 | case invalidValue(Any, String) 17 | /// An invalid collection element type was found (contains the type) 18 | case invalidCollectionElementType(Any) 19 | /// An invalid array element was found (contains the element, and its index) 20 | case invalidArrayElement(Any, Int) 21 | /// An invalid dictionary key type was found (contains the type) 22 | case invalidDictionaryKeyType(Any) 23 | /// An invalid dictionary key was found (contains the key) 24 | case invalidDictionaryKey(Any) 25 | /// An invalid dictionary value was found (contains the value, and its key) 26 | case invalidDictionaryValue(Any, String) 27 | } 28 | 29 | extension UnboxPathError: CustomStringConvertible { 30 | public var description: String { 31 | switch self { 32 | case .emptyKeyPath: 33 | return "Key path can't be empty." 34 | case .missingKey(let key): 35 | return "The key \"\(key)\" is missing." 36 | case .invalidValue(let value, let key): 37 | return "Invalid value (\(value)) for key \"\(key)\"." 38 | case .invalidCollectionElementType(let type): 39 | return "Invalid collection element type: \(type). Must be UnboxCompatible or Unboxable." 40 | case .invalidArrayElement(let element, let index): 41 | return "Invalid array element (\(element)) at index \(index)." 42 | case .invalidDictionaryKeyType(let type): 43 | return "Invalid dictionary key type: \(type). Must be either String or UnboxableKey." 44 | case .invalidDictionaryKey(let key): 45 | return "Invalid dictionary key: \(key)." 46 | case .invalidDictionaryValue(let value, let key): 47 | return "Invalid dictionary value (\(value)) for key \"\(key)\"." 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MoviesAppTests/Features/Search/SearchPresenterTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MoviesApp 3 | 4 | class SearchPresenterTest: BaseTestCase { 5 | 6 | var presenter: SearchPresenter! 7 | var view: SearchViewMock! 8 | var repository: SearchRepositoryMock! 9 | 10 | override func setUp() { 11 | super.setUp() 12 | 13 | view = SearchViewMock(with: self) 14 | repository = SearchRepositoryMock(with: self) 15 | presenter = SearchPresenter(view: view, repository: repository) 16 | } 17 | 18 | override func tearDown() { 19 | 20 | view = nil 21 | repository = nil 22 | presenter = nil 23 | super.tearDown() 24 | } 25 | 26 | func testOnViewWillAppearWithSuggestions() { 27 | repository.state = .success 28 | 29 | presenter.onViewWillAppear() 30 | 31 | verify(repository).getLastSearchResults(onSuccess: { _ in }) 32 | verify(view).show(suggestions: SuggestionBuilder.suggestions()) 33 | verifyOrder() 34 | } 35 | 36 | func testOnViewWillAppearWithNoSuggestions() { 37 | repository.state = .empty 38 | 39 | presenter.onViewWillAppear() 40 | 41 | verify(repository).getLastSearchResults(onSuccess: { _ in }) 42 | verifyOrder() 43 | } 44 | 45 | func testSearchWithSuccess() { 46 | repository.state = .success 47 | var movies: [Movie] = [] 48 | view.onMoviesSelected = { list in 49 | movies = list 50 | } 51 | 52 | presenter.onSearch("Batman") 53 | 54 | verify(view).showLoadingIndicator() 55 | verify(repository).searchMovies(with: "Batman", onSuccess: { _ in }, onError: { _ in }) 56 | verify(view).hideLoadingIndicator() 57 | verify(repository).getLastSearchResults(onSuccess: { _ in }) 58 | verify(view).show(suggestions: SuggestionBuilder.suggestions()) 59 | XCTAssertEqual(movies.count, MovieBuilder.movies().count) 60 | verifyOrder() 61 | } 62 | 63 | func testSearchWithFail() { 64 | repository.state = .failure 65 | 66 | presenter.onSearch("Batman") 67 | 68 | verify(view).showLoadingIndicator() 69 | verify(repository).searchMovies(with: "Batman", onSuccess: { _ in }, onError: { _ in }) 70 | verify(view).hideLoadingIndicator() 71 | verify(view).show(message: "error") 72 | verifyOrder() 73 | } 74 | 75 | func testSearchWithNilText() { 76 | presenter.onSearch(nil) 77 | 78 | verify(repository, .never).searchMovies(with: "", onSuccess: { _ in }, onError: { _ in }) 79 | verify(view, .never).showLoadingIndicator() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/UnboxableCollection.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | // MARK: - Protocol 10 | 11 | /// Protocol used to enable collections to be unboxed. Default implementations exist for Array & Dictionary 12 | public protocol UnboxableCollection: Collection, UnboxCompatible { 13 | /// The value type that this collection contains 14 | associatedtype UnboxValue 15 | 16 | /// Unbox a value into a collection, optionally allowing invalid elements 17 | static func unbox(value: Any, allowInvalidElements: Bool, transformer: T) throws -> Self? where T.UnboxedElement == UnboxValue 18 | } 19 | 20 | // MARK: - Default implementations 21 | 22 | // Default implementation of `UnboxCompatible` for collections 23 | public extension UnboxableCollection { 24 | public static func unbox(value: Any, allowInvalidCollectionElements: Bool) throws -> Self? { 25 | if let matchingCollection = value as? Self { 26 | return matchingCollection 27 | } 28 | 29 | if let unboxableType = UnboxValue.self as? Unboxable.Type { 30 | let transformer = UnboxCollectionElementClosureTransformer() { element in 31 | let unboxer = Unboxer(dictionary: element) 32 | return try unboxableType.init(unboxer: unboxer) as? UnboxValue 33 | } 34 | 35 | return try self.unbox(value: value, allowInvalidElements: allowInvalidCollectionElements, transformer: transformer) 36 | } 37 | 38 | if let unboxCompatibleType = UnboxValue.self as? UnboxCompatible.Type { 39 | let transformer = UnboxCollectionElementClosureTransformer() { element in 40 | return try unboxCompatibleType.unbox(value: element, allowInvalidCollectionElements: allowInvalidCollectionElements) as? UnboxValue 41 | } 42 | 43 | return try self.unbox(value: value, allowInvalidElements: allowInvalidCollectionElements, transformer: transformer) 44 | } 45 | 46 | throw UnboxPathError.invalidCollectionElementType(UnboxValue.self) 47 | } 48 | } 49 | 50 | // MARK: - Utility types 51 | 52 | private class UnboxCollectionElementClosureTransformer: UnboxCollectionElementTransformer { 53 | private let closure: (I) throws -> O? 54 | 55 | init(closure: @escaping (I) throws -> O?) { 56 | self.closure = closure 57 | } 58 | 59 | func unbox(element: I, allowInvalidCollectionElements: Bool) throws -> O? { 60 | return try self.closure(element) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Pods/TableKit/Sources/TableCellRegisterer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | // this software and associated documentation files (the "Software"), to deal in 6 | // the Software without restriction, including without limitation the rights to 7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | // the Software, and to permit persons to whom the Software is furnished to do so, 9 | // subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import UIKit 22 | 23 | class TableCellRegisterer { 24 | 25 | private var registeredIds = Set() 26 | private weak var tableView: UITableView? 27 | 28 | init(tableView: UITableView?) { 29 | self.tableView = tableView 30 | } 31 | 32 | func register(cellType: AnyClass, forCellReuseIdentifier reuseIdentifier: String) { 33 | 34 | if registeredIds.contains(reuseIdentifier) { 35 | return 36 | } 37 | 38 | // check if cell is already registered, probably cell has been registered by storyboard 39 | if tableView?.dequeueReusableCell(withIdentifier: reuseIdentifier) != nil { 40 | 41 | registeredIds.insert(reuseIdentifier) 42 | return 43 | } 44 | 45 | let bundle = Bundle(for: cellType) 46 | 47 | // we hope that cell's xib file has name that equals to cell's class name 48 | // in that case we could register nib 49 | if let _ = bundle.path(forResource: reuseIdentifier, ofType: "nib") { 50 | tableView?.register(UINib(nibName: reuseIdentifier, bundle: bundle), forCellReuseIdentifier: reuseIdentifier) 51 | // otherwise, register cell class 52 | } else { 53 | tableView?.register(cellType, forCellReuseIdentifier: reuseIdentifier) 54 | } 55 | 56 | registeredIds.insert(reuseIdentifier) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Dictionary+Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Extension making `Dictionary` an unboxable collection 10 | extension Dictionary: UnboxableCollection { 11 | public typealias UnboxValue = Value 12 | 13 | public static func unbox(value: Any, allowInvalidElements: Bool, transformer: T) throws -> Dictionary? where T.UnboxedElement == UnboxValue { 14 | guard let dictionary = value as? [String : T.UnboxRawElement] else { 15 | return nil 16 | } 17 | 18 | let keyTransform = try self.makeKeyTransform() 19 | 20 | return try dictionary.map(allowInvalidElements: allowInvalidElements) { key, value in 21 | guard let unboxedKey = keyTransform(key) else { 22 | throw UnboxPathError.invalidDictionaryKey(key) 23 | } 24 | 25 | guard let unboxedValue = try transformer.unbox(element: value, allowInvalidCollectionElements: allowInvalidElements) else { 26 | throw UnboxPathError.invalidDictionaryValue(value, key) 27 | } 28 | 29 | return (unboxedKey, unboxedValue) 30 | } 31 | } 32 | 33 | private static func makeKeyTransform() throws -> (String) -> Key? { 34 | if Key.self is String.Type { 35 | return { $0 as? Key } 36 | } 37 | 38 | if let keyType = Key.self as? UnboxableKey.Type { 39 | return { keyType.transform(unboxedKey: $0) as? Key } 40 | } 41 | 42 | throw UnboxPathError.invalidDictionaryKeyType(Key.self) 43 | } 44 | } 45 | 46 | /// Extension making `Dictionary` an unbox path node 47 | extension Dictionary: UnboxPathNode { 48 | func unboxPathValue(forKey key: String) -> Any? { 49 | return self[key as! Key] 50 | } 51 | } 52 | 53 | // MARK: - Utilities 54 | 55 | private extension Dictionary { 56 | func map(allowInvalidElements: Bool, transform: (Key, Value) throws -> (K, V)?) throws -> [K : V]? { 57 | var transformedDictionary = [K : V]() 58 | 59 | for (key, value) in self { 60 | do { 61 | guard let transformed = try transform(key, value) else { 62 | if allowInvalidElements { 63 | continue 64 | } 65 | 66 | return nil 67 | } 68 | 69 | transformedDictionary[transformed.0] = transformed.1 70 | } catch { 71 | if !allowInvalidElements { 72 | throw error 73 | } 74 | } 75 | } 76 | 77 | return transformedDictionary 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Pods/Alamofire/Source/Notifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notifications.swift 3 | // 4 | // Copyright (c) 2014-2017 Alamofire Software Foundation (http://alamofire.org/) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension Notification.Name { 28 | /// Used as a namespace for all `URLSessionTask` related notifications. 29 | public struct Task { 30 | /// Posted when a `URLSessionTask` is resumed. The notification `object` contains the resumed `URLSessionTask`. 31 | public static let DidResume = Notification.Name(rawValue: "org.alamofire.notification.name.task.didResume") 32 | 33 | /// Posted when a `URLSessionTask` is suspended. The notification `object` contains the suspended `URLSessionTask`. 34 | public static let DidSuspend = Notification.Name(rawValue: "org.alamofire.notification.name.task.didSuspend") 35 | 36 | /// Posted when a `URLSessionTask` is cancelled. The notification `object` contains the cancelled `URLSessionTask`. 37 | public static let DidCancel = Notification.Name(rawValue: "org.alamofire.notification.name.task.didCancel") 38 | 39 | /// Posted when a `URLSessionTask` is completed. The notification `object` contains the completed `URLSessionTask`. 40 | public static let DidComplete = Notification.Name(rawValue: "org.alamofire.notification.name.task.didComplete") 41 | } 42 | } 43 | 44 | // MARK: - 45 | 46 | extension Notification { 47 | /// Used as a namespace for all `Notification` user info dictionary keys. 48 | public struct Key { 49 | /// User info dictionary key representing the `URLSessionTask` associated with the notification. 50 | public static let Task = "org.alamofire.notification.key.task" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MoviesApp/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 | -------------------------------------------------------------------------------- /Pods/TableKit/Sources/TableKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | // this software and associated documentation files (the "Software"), to deal in 6 | // the Software without restriction, including without limitation the rights to 7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | // the Software, and to permit persons to whom the Software is furnished to do so, 9 | // subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import UIKit 22 | 23 | struct TableKitNotifications { 24 | static let CellAction = "TableKitNotificationsCellAction" 25 | } 26 | 27 | public protocol RowConfigurable { 28 | 29 | func configure(_ cell: UITableViewCell) 30 | } 31 | 32 | public protocol RowActionable { 33 | 34 | var editingActions: [UITableViewRowAction]? { get } 35 | func isEditingAllowed(forIndexPath indexPath: IndexPath) -> Bool 36 | 37 | func invoke(action: TableRowActionType, cell: UITableViewCell?, path: IndexPath, userInfo: [AnyHashable: Any]?) -> Any? 38 | func has(action: TableRowActionType) -> Bool 39 | } 40 | 41 | public protocol RowHashable { 42 | 43 | var hashValue: Int { get } 44 | } 45 | 46 | public protocol Row: RowConfigurable, RowActionable, RowHashable { 47 | 48 | var reuseIdentifier: String { get } 49 | var cellType: AnyClass { get } 50 | 51 | var estimatedHeight: CGFloat? { get } 52 | var defaultHeight: CGFloat? { get } 53 | } 54 | 55 | public enum TableRowActionType { 56 | 57 | case click 58 | case clickDelete 59 | case select 60 | case deselect 61 | case willSelect 62 | case willDisplay 63 | case shouldHighlight 64 | case height 65 | case canEdit 66 | case configure 67 | case custom(String) 68 | 69 | var key: String { 70 | 71 | switch (self) { 72 | case .custom(let key): 73 | return key 74 | default: 75 | return "_\(self)" 76 | } 77 | } 78 | } 79 | 80 | public protocol RowHeightCalculator { 81 | 82 | func height(forRow row: Row, at indexPath: IndexPath) -> CGFloat 83 | func estimatedHeight(forRow row: Row, at indexPath: IndexPath) -> CGFloat 84 | 85 | func invalidate() 86 | } 87 | -------------------------------------------------------------------------------- /MoviesAppTests/Slate/CallHandler.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | fileprivate enum State { 4 | case none, verify 5 | } 6 | 7 | class CallHandler { 8 | 9 | private var state = State.none 10 | private var invoked = Invoke.once 11 | private var callHistory: [String: UInt] = [:] 12 | private let testCase: BaseTestCase 13 | private var original: [Equivalent] = [] 14 | private var verified: [Equivalent] = [] 15 | private let verifyOrder: Bool 16 | 17 | init(withTestCase testCase: BaseTestCase, verifyOrder: Bool = true) { 18 | self.testCase = testCase 19 | self.verifyOrder = verifyOrder 20 | } 21 | 22 | func verify(invoked: Invoke) { 23 | self.invoked = invoked 24 | state = .verify 25 | } 26 | 27 | @discardableResult 28 | func accept(function: String, file: String, line: UInt) -> Self { 29 | switch state { 30 | case .none: recordCallHistory(ofFunction: function) 31 | case .verify: 32 | verifyCall(function: function, file: file, line: line) 33 | state = .none 34 | } 35 | return self 36 | } 37 | 38 | @discardableResult 39 | func join(with: Equivalent) -> Self { 40 | switch state { 41 | case .none: original.append(with) 42 | case .verify: verified.append(with) 43 | } 44 | return self 45 | } 46 | 47 | func check(function: String, file: String, line: UInt) { 48 | guard state == .verify else { return } 49 | 50 | for (index, value) in original.enumerated() { 51 | let verify = verified[index] 52 | if value != verify { 53 | doFail("Arguments are not equal for the method \(function)", file: file, line: line) 54 | } 55 | } 56 | state = .none 57 | } 58 | 59 | private func recordCallHistory(ofFunction function: String) { 60 | if let numberOfCalles = callHistory[function] { 61 | callHistory[function] = numberOfCalles + 1 62 | } else { 63 | callHistory[function] = 1 64 | } 65 | if verifyOrder { 66 | testCase.callHistory.append(function) 67 | } 68 | } 69 | 70 | private func verifyCall(function: String, file: String, line: UInt) { 71 | let timesCalled = callHistory[function] ?? 0 72 | var isCalledLikeExpected = false 73 | 74 | switch invoked { 75 | case .once: 76 | isCalledLikeExpected = timesCalled == 1 77 | if verifyOrder { 78 | testCase.callMockHistory.append(function) 79 | } 80 | case .times(let numberOfCalles): 81 | isCalledLikeExpected = timesCalled == numberOfCalles 82 | if verifyOrder { 83 | testCase.callMockHistory.append(function) 84 | } 85 | case .never: 86 | isCalledLikeExpected = timesCalled == 0 87 | } 88 | 89 | if !isCalledLikeExpected { 90 | doFail("Expected \(0) to be called %d times. It is actually called \(timesCalled) times", file: file, line: line) 91 | } 92 | } 93 | 94 | private func doFail(_ message: String, file: String, line: UInt) { 95 | testCase.recordFailure(withDescription: message, inFile: file, atLine: Int(line), expected: true) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Pods/TableKit/Sources/TableRowAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | // this software and associated documentation files (the "Software"), to deal in 6 | // the Software without restriction, including without limitation the rights to 7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | // the Software, and to permit persons to whom the Software is furnished to do so, 9 | // subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import UIKit 22 | 23 | open class TableRowActionOptions where CellType: UITableViewCell { 24 | 25 | open let item: CellType.T 26 | open let cell: CellType? 27 | open let indexPath: IndexPath 28 | open let userInfo: [AnyHashable: Any]? 29 | 30 | init(item: CellType.T, cell: CellType?, path: IndexPath, userInfo: [AnyHashable: Any]?) { 31 | 32 | self.item = item 33 | self.cell = cell 34 | self.indexPath = path 35 | self.userInfo = userInfo 36 | } 37 | } 38 | 39 | private enum TableRowActionHandler where CellType: UITableViewCell { 40 | 41 | case voidAction((TableRowActionOptions) -> Void) 42 | case action((TableRowActionOptions) -> Any?) 43 | 44 | func invoke(withOptions options: TableRowActionOptions) -> Any? { 45 | 46 | switch self { 47 | case .voidAction(let handler): 48 | return handler(options) 49 | case .action(let handler): 50 | return handler(options) 51 | } 52 | } 53 | } 54 | 55 | open class TableRowAction where CellType: UITableViewCell { 56 | 57 | open var id: String? 58 | open let type: TableRowActionType 59 | private let handler: TableRowActionHandler 60 | 61 | public init(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> Void) { 62 | 63 | self.type = type 64 | self.handler = .voidAction(handler) 65 | } 66 | 67 | public init(_ key: String, handler: @escaping (_ options: TableRowActionOptions) -> Void) { 68 | 69 | self.type = .custom(key) 70 | self.handler = .voidAction(handler) 71 | } 72 | 73 | public init(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> T) { 74 | 75 | self.type = type 76 | self.handler = .action(handler) 77 | } 78 | 79 | public func invokeActionOn(cell: UITableViewCell?, item: CellType.T, path: IndexPath, userInfo: [AnyHashable: Any]?) -> Any? { 80 | 81 | return handler.invoke(withOptions: TableRowActionOptions(item: item, cell: cell as? CellType, path: path, userInfo: userInfo)) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Pods/TableKit/Sources/TablePrototypeCellHeightCalculator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | // this software and associated documentation files (the "Software"), to deal in 6 | // the Software without restriction, including without limitation the rights to 7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | // the Software, and to permit persons to whom the Software is furnished to do so, 9 | // subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import UIKit 22 | 23 | open class TablePrototypeCellHeightCalculator: RowHeightCalculator { 24 | 25 | private(set) weak var tableView: UITableView? 26 | private var prototypes = [String: UITableViewCell]() 27 | private var cachedHeights = [Int: CGFloat]() 28 | private var separatorHeight = 1 / UIScreen.main.scale 29 | 30 | public init(tableView: UITableView?) { 31 | self.tableView = tableView 32 | } 33 | 34 | open func height(forRow row: Row, at indexPath: IndexPath) -> CGFloat { 35 | 36 | guard let tableView = tableView else { return 0 } 37 | 38 | let hash = row.hashValue ^ Int(tableView.bounds.size.width).hashValue 39 | 40 | if let height = cachedHeights[hash] { 41 | return height 42 | } 43 | 44 | var prototypeCell = prototypes[row.reuseIdentifier] 45 | if prototypeCell == nil { 46 | 47 | prototypeCell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier) 48 | prototypes[row.reuseIdentifier] = prototypeCell 49 | } 50 | 51 | guard let cell = prototypeCell else { return 0 } 52 | 53 | cell.prepareForReuse() 54 | row.configure(cell) 55 | 56 | cell.bounds = CGRect(x: 0, y: 0, width: tableView.bounds.size.width, height: cell.bounds.height) 57 | cell.setNeedsLayout() 58 | cell.layoutIfNeeded() 59 | 60 | let height = cell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height + (tableView.separatorStyle != .none ? separatorHeight : 0) 61 | 62 | cachedHeights[hash] = height 63 | 64 | return height 65 | } 66 | 67 | open func estimatedHeight(forRow row: Row, at indexPath: IndexPath) -> CGFloat { 68 | 69 | guard let tableView = tableView else { return 0 } 70 | 71 | let hash = row.hashValue ^ Int(tableView.bounds.size.width).hashValue 72 | 73 | if let height = cachedHeights[hash] { 74 | return height 75 | } 76 | 77 | if let estimatedHeight = row.estimatedHeight , estimatedHeight > 0 { 78 | return estimatedHeight 79 | } 80 | 81 | return UITableViewAutomaticDimension 82 | } 83 | 84 | open func invalidate() { 85 | cachedHeights.removeAll() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Pods/TableKit/Sources/TableSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | // this software and associated documentation files (the "Software"), to deal in 6 | // the Software without restriction, including without limitation the rights to 7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | // the Software, and to permit persons to whom the Software is furnished to do so, 9 | // subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import UIKit 22 | 23 | open class TableSection { 24 | 25 | open private(set) var rows = [Row]() 26 | 27 | open var headerTitle: String? 28 | open var footerTitle: String? 29 | open var indexTitle: String? 30 | 31 | open var headerView: UIView? 32 | open var footerView: UIView? 33 | 34 | open var headerHeight: CGFloat? = nil 35 | open var footerHeight: CGFloat? = nil 36 | 37 | open var numberOfRows: Int { 38 | return rows.count 39 | } 40 | 41 | open var isEmpty: Bool { 42 | return rows.isEmpty 43 | } 44 | 45 | public init(rows: [Row]? = nil) { 46 | 47 | if let initialRows = rows { 48 | self.rows.append(contentsOf: initialRows) 49 | } 50 | } 51 | 52 | public convenience init(headerTitle: String?, footerTitle: String?, rows: [Row]? = nil) { 53 | self.init(rows: rows) 54 | 55 | self.headerTitle = headerTitle 56 | self.footerTitle = footerTitle 57 | } 58 | 59 | public convenience init(headerView: UIView?, footerView: UIView?, rows: [Row]? = nil) { 60 | self.init(rows: rows) 61 | 62 | self.headerView = headerView 63 | self.footerView = footerView 64 | } 65 | 66 | // MARK: - Public - 67 | 68 | open func clear() { 69 | rows.removeAll() 70 | } 71 | 72 | open func append(row: Row) { 73 | append(rows: [row]) 74 | } 75 | 76 | open func append(rows: [Row]) { 77 | self.rows.append(contentsOf: rows) 78 | } 79 | 80 | open func insert(row: Row, at index: Int) { 81 | rows.insert(row, at: index) 82 | } 83 | 84 | open func insert(rows: [Row], at index: Int) { 85 | self.rows.insert(contentsOf: rows, at: index) 86 | } 87 | 88 | open func replace(rowAt index: Int, with row: Row) { 89 | rows[index] = row 90 | } 91 | 92 | open func delete(rowAt index: Int) { 93 | rows.remove(at: index) 94 | } 95 | 96 | open func remove(rowAt index: Int) { 97 | rows.remove(at: index) 98 | } 99 | 100 | // MARK: - deprecated methods - 101 | 102 | @available(*, deprecated, message: "Use 'delete(rowAt:)' method instead") 103 | open func delete(index: Int) { 104 | rows.remove(at: index) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MoviesApp/Pods-MoviesApp-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 5 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 6 | 7 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 8 | 9 | install_framework() 10 | { 11 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 12 | local source="${BUILT_PRODUCTS_DIR}/$1" 13 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 14 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 15 | elif [ -r "$1" ]; then 16 | local source="$1" 17 | fi 18 | 19 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 20 | 21 | if [ -L "${source}" ]; then 22 | echo "Symlinked..." 23 | source="$(readlink "${source}")" 24 | fi 25 | 26 | # use filter instead of exclude so missing patterns dont' throw errors 27 | echo "rsync -av --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 28 | rsync -av --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 29 | 30 | local basename 31 | basename="$(basename -s .framework "$1")" 32 | binary="${destination}/${basename}.framework/${basename}" 33 | if ! [ -r "$binary" ]; then 34 | binary="${destination}/${basename}" 35 | fi 36 | 37 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 38 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 39 | strip_invalid_archs "$binary" 40 | fi 41 | 42 | # Resign the code if required by the build settings to avoid unstable apps 43 | code_sign_if_enabled "${destination}/$(basename "$1")" 44 | 45 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 46 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 47 | local swift_runtime_libs 48 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) 49 | for lib in $swift_runtime_libs; do 50 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 51 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 52 | code_sign_if_enabled "${destination}/${lib}" 53 | done 54 | fi 55 | } 56 | 57 | # Signs a framework with the provided identity 58 | code_sign_if_enabled() { 59 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 60 | # Use the current code_sign_identitiy 61 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 62 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements '$1'" 63 | 64 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 65 | code_sign_cmd="$code_sign_cmd &" 66 | fi 67 | echo "$code_sign_cmd" 68 | eval "$code_sign_cmd" 69 | fi 70 | } 71 | 72 | # Strip invalid architectures 73 | strip_invalid_archs() { 74 | binary="$1" 75 | # Get architectures for current file 76 | archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" 77 | stripped="" 78 | for arch in $archs; do 79 | if ! [[ "${VALID_ARCHS}" == *"$arch"* ]]; then 80 | # Strip non-valid architectures in-place 81 | lipo -remove "$arch" -output "$binary" "$binary" || exit 1 82 | stripped="$stripped $arch" 83 | fi 84 | done 85 | if [[ "$stripped" ]]; then 86 | echo "Stripped $binary of architectures:$stripped" 87 | fi 88 | } 89 | 90 | 91 | if [[ "$CONFIGURATION" == "Debug" ]]; then 92 | install_framework "$BUILT_PRODUCTS_DIR/Alamofire/Alamofire.framework" 93 | install_framework "$BUILT_PRODUCTS_DIR/MBProgressHUD/MBProgressHUD.framework" 94 | install_framework "$BUILT_PRODUCTS_DIR/SwiftyUserDefaults/SwiftyUserDefaults.framework" 95 | install_framework "$BUILT_PRODUCTS_DIR/TableKit/TableKit.framework" 96 | install_framework "$BUILT_PRODUCTS_DIR/Unbox/Unbox.framework" 97 | fi 98 | if [[ "$CONFIGURATION" == "Release" ]]; then 99 | install_framework "$BUILT_PRODUCTS_DIR/Alamofire/Alamofire.framework" 100 | install_framework "$BUILT_PRODUCTS_DIR/MBProgressHUD/MBProgressHUD.framework" 101 | install_framework "$BUILT_PRODUCTS_DIR/SwiftyUserDefaults/SwiftyUserDefaults.framework" 102 | install_framework "$BUILT_PRODUCTS_DIR/TableKit/TableKit.framework" 103 | install_framework "$BUILT_PRODUCTS_DIR/Unbox/Unbox.framework" 104 | fi 105 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 106 | wait 107 | fi 108 | -------------------------------------------------------------------------------- /Pods/Unbox/Sources/Unbox.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Unbox 3 | * Copyright (c) 2015-2017 John Sundell 4 | * Licensed under the MIT license, see LICENSE file 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Unbox a JSON dictionary into a model `T`. Throws `UnboxError`. 10 | public func unbox(dictionary: UnboxableDictionary) throws -> T { 11 | return try Unboxer(dictionary: dictionary).performUnboxing() 12 | } 13 | 14 | /// Unbox a JSON dictionary into a model `T` beginning at a certain key. Throws `UnboxError`. 15 | public func unbox(dictionary: UnboxableDictionary, atKey key: String) throws -> T { 16 | let container: UnboxContainer = try unbox(dictionary: dictionary, context: .key(key)) 17 | return container.model 18 | } 19 | 20 | /// Unbox a JSON dictionary into a model `T` beginning at a certain key path. Throws `UnboxError`. 21 | public func unbox(dictionary: UnboxableDictionary, atKeyPath keyPath: String) throws -> T { 22 | let container: UnboxContainer = try unbox(dictionary: dictionary, context: .keyPath(keyPath)) 23 | return container.model 24 | } 25 | 26 | /// Unbox an array of JSON dictionaries into an array of `T`, optionally allowing invalid elements. Throws `UnboxError`. 27 | public func unbox(dictionaries: [UnboxableDictionary], allowInvalidElements: Bool = false) throws -> [T] { 28 | return try dictionaries.map(allowInvalidElements: allowInvalidElements, transform: unbox) 29 | } 30 | 31 | /// Unbox an array JSON dictionary into an array of model `T` beginning at a certain key, optionally allowing invalid elements. Throws `UnboxError`. 32 | public func unbox(dictionary: UnboxableDictionary, atKey key: String, allowInvalidElements: Bool = false) throws -> [T] { 33 | let container: UnboxArrayContainer = try unbox(dictionary: dictionary, context: (.key(key), allowInvalidElements)) 34 | return container.models 35 | } 36 | 37 | /// Unbox an array JSON dictionary into an array of model `T` beginning at a certain key path, optionally allowing invalid elements. Throws `UnboxError`. 38 | public func unbox(dictionary: UnboxableDictionary, atKeyPath keyPath: String, allowInvalidElements: Bool = false) throws -> [T] { 39 | let container: UnboxArrayContainer = try unbox(dictionary: dictionary, context: (.keyPath(keyPath), allowInvalidElements)) 40 | return container.models 41 | } 42 | 43 | /// Unbox binary data into a model `T`. Throws `UnboxError`. 44 | public func unbox(data: Data) throws -> T { 45 | return try data.unbox() 46 | } 47 | 48 | /// Unbox binary data into an array of `T`, optionally allowing invalid elements. Throws `UnboxError`. 49 | public func unbox(data: Data, atKeyPath keyPath: String? = nil, allowInvalidElements: Bool = false) throws -> [T] { 50 | if let keyPath = keyPath { 51 | return try unbox(dictionary: JSONSerialization.unbox(data: data), atKeyPath: keyPath, allowInvalidElements: allowInvalidElements) 52 | } 53 | 54 | return try data.unbox(allowInvalidElements: allowInvalidElements) 55 | } 56 | 57 | /// Unbox a JSON dictionary into a model `T` using a required contextual object. Throws `UnboxError`. 58 | public func unbox(dictionary: UnboxableDictionary, context: T.UnboxContext) throws -> T { 59 | return try Unboxer(dictionary: dictionary).performUnboxing(context: context) 60 | } 61 | 62 | /// Unbox an array of JSON dictionaries into an array of `T` using a required contextual object, optionally allowing invalid elements. Throws `UnboxError`. 63 | public func unbox(dictionaries: [UnboxableDictionary], context: T.UnboxContext, allowInvalidElements: Bool = false) throws -> [T] { 64 | return try dictionaries.map(allowInvalidElements: allowInvalidElements, transform: { 65 | try unbox(dictionary: $0, context: context) 66 | }) 67 | } 68 | 69 | /// Unbox binary data into a model `T` using a required contextual object. Throws `UnboxError`. 70 | public func unbox(data: Data, context: T.UnboxContext) throws -> T { 71 | return try data.unbox(context: context) 72 | } 73 | 74 | /// Unbox binary data into an array of `T` using a required contextual object, optionally allowing invalid elements. Throws `UnboxError`. 75 | public func unbox(data: Data, context: T.UnboxContext, allowInvalidElements: Bool = false) throws -> [T] { 76 | return try data.unbox(context: context, allowInvalidElements: allowInvalidElements) 77 | } 78 | 79 | /// Unbox binary data into a dictionary of type `[String: T]`. Throws `UnboxError`. 80 | public func unbox(data: Data) throws -> [String: T] { 81 | let dictionary : [String: [String: Any]] = try JSONSerialization.unbox(data: data) 82 | return try unbox(dictionary: dictionary) 83 | } 84 | 85 | /// Unbox `UnboxableDictionary` into a dictionary of type `[String: T]` where `T` is `Unboxable`. Throws `UnboxError`. 86 | public func unbox(dictionary: UnboxableDictionary) throws -> [String: T] { 87 | var mappedDictionary = [String: T]() 88 | try dictionary.forEach { key, value in 89 | guard let innerDictionary = value as? UnboxableDictionary else { 90 | throw UnboxError.invalidData 91 | } 92 | let data : T = try unbox(dictionary: innerDictionary) 93 | mappedDictionary[key] = data 94 | } 95 | return mappedDictionary 96 | } 97 | -------------------------------------------------------------------------------- /Pods/TableKit/Sources/TableRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | // this software and associated documentation files (the "Software"), to deal in 6 | // the Software without restriction, including without limitation the rights to 7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | // the Software, and to permit persons to whom the Software is furnished to do so, 9 | // subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import UIKit 22 | 23 | open class TableRow: Row where CellType: UITableViewCell { 24 | 25 | open let item: CellType.T 26 | private lazy var actions = [String: [TableRowAction]]() 27 | private(set) open var editingActions: [UITableViewRowAction]? 28 | 29 | open var hashValue: Int { 30 | return ObjectIdentifier(self).hashValue 31 | } 32 | 33 | open var reuseIdentifier: String { 34 | return CellType.reuseIdentifier 35 | } 36 | 37 | open var estimatedHeight: CGFloat? { 38 | return CellType.estimatedHeight 39 | } 40 | 41 | open var defaultHeight: CGFloat? { 42 | return CellType.defaultHeight 43 | } 44 | 45 | open var cellType: AnyClass { 46 | return CellType.self 47 | } 48 | 49 | public init(item: CellType.T, actions: [TableRowAction]? = nil, editingActions: [UITableViewRowAction]? = nil) { 50 | 51 | self.item = item 52 | self.editingActions = editingActions 53 | actions?.forEach { on($0) } 54 | } 55 | 56 | // MARK: - RowConfigurable - 57 | 58 | open func configure(_ cell: UITableViewCell) { 59 | 60 | (cell as? CellType)?.configure(with: item) 61 | } 62 | 63 | // MARK: - RowActionable - 64 | 65 | open func invoke(action: TableRowActionType, cell: UITableViewCell?, path: IndexPath, userInfo: [AnyHashable: Any]? = nil) -> Any? { 66 | 67 | return actions[action.key]?.flatMap({ $0.invokeActionOn(cell: cell, item: item, path: path, userInfo: userInfo) }).last 68 | } 69 | 70 | open func has(action: TableRowActionType) -> Bool { 71 | 72 | return actions[action.key] != nil 73 | } 74 | 75 | open func isEditingAllowed(forIndexPath indexPath: IndexPath) -> Bool { 76 | 77 | if actions[TableRowActionType.canEdit.key] != nil { 78 | return invoke(action: .canEdit, cell: nil, path: indexPath) as? Bool ?? false 79 | } 80 | return editingActions?.isEmpty == false || actions[TableRowActionType.clickDelete.key] != nil 81 | } 82 | 83 | // MARK: - actions - 84 | 85 | @discardableResult 86 | open func on(_ action: TableRowAction) -> Self { 87 | 88 | if actions[action.type.key] == nil { 89 | actions[action.type.key] = [TableRowAction]() 90 | } 91 | actions[action.type.key]?.append(action) 92 | 93 | return self 94 | } 95 | 96 | @discardableResult 97 | open func on(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> T) -> Self { 98 | 99 | return on(TableRowAction(type, handler: handler)) 100 | } 101 | 102 | @discardableResult 103 | open func on(_ key: String, handler: @escaping (_ options: TableRowActionOptions) -> ()) -> Self { 104 | 105 | return on(TableRowAction(.custom(key), handler: handler)) 106 | } 107 | 108 | open func removeAllActions() { 109 | 110 | actions.removeAll() 111 | } 112 | 113 | open func removeAction(forActionId actionId: String) { 114 | 115 | for (key, value) in actions { 116 | if let actionIndex = value.index(where: { $0.id == actionId }) { 117 | actions[key]?.remove(at: actionIndex) 118 | } 119 | } 120 | } 121 | 122 | // MARK: - deprecated actions - 123 | 124 | @available(*, deprecated, message: "Use 'on' method instead") 125 | @discardableResult 126 | open func action(_ action: TableRowAction) -> Self { 127 | 128 | return on(action) 129 | } 130 | 131 | @available(*, deprecated, message: "Use 'on' method instead") 132 | @discardableResult 133 | open func action(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> T) -> Self { 134 | 135 | return on(TableRowAction(type, handler: handler)) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MoviesApp/Pods-MoviesApp-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 5 | 6 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 7 | > "$RESOURCES_TO_COPY" 8 | 9 | XCASSET_FILES=() 10 | 11 | case "${TARGETED_DEVICE_FAMILY}" in 12 | 1,2) 13 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 14 | ;; 15 | 1) 16 | TARGET_DEVICE_ARGS="--target-device iphone" 17 | ;; 18 | 2) 19 | TARGET_DEVICE_ARGS="--target-device ipad" 20 | ;; 21 | 3) 22 | TARGET_DEVICE_ARGS="--target-device tv" 23 | ;; 24 | *) 25 | TARGET_DEVICE_ARGS="--target-device mac" 26 | ;; 27 | esac 28 | 29 | install_resource() 30 | { 31 | if [[ "$1" = /* ]] ; then 32 | RESOURCE_PATH="$1" 33 | else 34 | RESOURCE_PATH="${PODS_ROOT}/$1" 35 | fi 36 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 37 | cat << EOM 38 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 39 | EOM 40 | exit 1 41 | fi 42 | case $RESOURCE_PATH in 43 | *.storyboard) 44 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" 45 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 46 | ;; 47 | *.xib) 48 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" 49 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 50 | ;; 51 | *.framework) 52 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 53 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 54 | echo "rsync -av $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 55 | rsync -av "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 56 | ;; 57 | *.xcdatamodel) 58 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" 59 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 60 | ;; 61 | *.xcdatamodeld) 62 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" 63 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 64 | ;; 65 | *.xcmappingmodel) 66 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" 67 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 68 | ;; 69 | *.xcassets) 70 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 71 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 72 | ;; 73 | *) 74 | echo "$RESOURCE_PATH" 75 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 76 | ;; 77 | esac 78 | } 79 | 80 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 81 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 82 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 83 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 84 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 85 | fi 86 | rm -f "$RESOURCES_TO_COPY" 87 | 88 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] 89 | then 90 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 91 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 92 | while read line; do 93 | if [[ $line != "${PODS_ROOT}*" ]]; then 94 | XCASSET_FILES+=("$line") 95 | fi 96 | done <<<"$OTHER_XCASSETS" 97 | 98 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 99 | fi 100 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MoviesApp/Pods-MoviesApp-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## Alamofire 5 | 6 | Copyright (c) 2014-2017 Alamofire Software Foundation (http://alamofire.org/) 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | 27 | ## MBProgressHUD 28 | 29 | Copyright © 2009-2016 Matej Bukovinski 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in 39 | all copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 47 | THE SOFTWARE. 48 | 49 | ## SwiftyUserDefaults 50 | 51 | The MIT License (MIT) 52 | 53 | Copyright (c) 2015-2016 Radosław Pietruszewski 54 | 55 | Permission is hereby granted, free of charge, to any person obtaining a copy 56 | of this software and associated documentation files (the "Software"), to deal 57 | in the Software without restriction, including without limitation the rights 58 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 59 | copies of the Software, and to permit persons to whom the Software is 60 | furnished to do so, subject to the following conditions: 61 | 62 | The above copyright notice and this permission notice shall be included in all 63 | copies or substantial portions of the Software. 64 | 65 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 66 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 67 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 68 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 69 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 70 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 71 | SOFTWARE. 72 | 73 | 74 | ## TableKit 75 | 76 | Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 77 | 78 | Permission is hereby granted, free of charge, to any person obtaining a copy 79 | of this software and associated documentation files (the "Software"), to deal 80 | in the Software without restriction, including without limitation the rights 81 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 82 | copies of the Software, and to permit persons to whom the Software is 83 | furnished to do so, subject to the following conditions: 84 | 85 | The above copyright notice and this permission notice shall be included in 86 | all copies or substantial portions of the Software. 87 | 88 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 89 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 90 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 91 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 92 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 93 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 94 | THE SOFTWARE. 95 | 96 | ## Unbox 97 | 98 | The MIT License (MIT) 99 | 100 | Copyright (c) 2015 John Sundell 101 | 102 | Permission is hereby granted, free of charge, to any person obtaining a copy 103 | of this software and associated documentation files (the "Software"), to deal 104 | in the Software without restriction, including without limitation the rights 105 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 106 | copies of the Software, and to permit persons to whom the Software is 107 | furnished to do so, subject to the following conditions: 108 | 109 | The above copyright notice and this permission notice shall be included in all 110 | copies or substantial portions of the Software. 111 | 112 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 113 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 114 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 115 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 116 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 117 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 118 | SOFTWARE. 119 | 120 | 121 | Generated by CocoaPods - https://cocoapods.org 122 | -------------------------------------------------------------------------------- /MoviesApp/Base.lproj/SearchScreen.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 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Pods/Alamofire/Source/Timeline.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Timeline.swift 3 | // 4 | // Copyright (c) 2014-2017 Alamofire Software Foundation (http://alamofire.org/) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// Responsible for computing the timing metrics for the complete lifecycle of a `Request`. 28 | public struct Timeline { 29 | /// The time the request was initialized. 30 | public let requestStartTime: CFAbsoluteTime 31 | 32 | /// The time the first bytes were received from or sent to the server. 33 | public let initialResponseTime: CFAbsoluteTime 34 | 35 | /// The time when the request was completed. 36 | public let requestCompletedTime: CFAbsoluteTime 37 | 38 | /// The time when the response serialization was completed. 39 | public let serializationCompletedTime: CFAbsoluteTime 40 | 41 | /// The time interval in seconds from the time the request started to the initial response from the server. 42 | public let latency: TimeInterval 43 | 44 | /// The time interval in seconds from the time the request started to the time the request completed. 45 | public let requestDuration: TimeInterval 46 | 47 | /// The time interval in seconds from the time the request completed to the time response serialization completed. 48 | public let serializationDuration: TimeInterval 49 | 50 | /// The time interval in seconds from the time the request started to the time response serialization completed. 51 | public let totalDuration: TimeInterval 52 | 53 | /// Creates a new `Timeline` instance with the specified request times. 54 | /// 55 | /// - parameter requestStartTime: The time the request was initialized. Defaults to `0.0`. 56 | /// - parameter initialResponseTime: The time the first bytes were received from or sent to the server. 57 | /// Defaults to `0.0`. 58 | /// - parameter requestCompletedTime: The time when the request was completed. Defaults to `0.0`. 59 | /// - parameter serializationCompletedTime: The time when the response serialization was completed. Defaults 60 | /// to `0.0`. 61 | /// 62 | /// - returns: The new `Timeline` instance. 63 | public init( 64 | requestStartTime: CFAbsoluteTime = 0.0, 65 | initialResponseTime: CFAbsoluteTime = 0.0, 66 | requestCompletedTime: CFAbsoluteTime = 0.0, 67 | serializationCompletedTime: CFAbsoluteTime = 0.0) 68 | { 69 | self.requestStartTime = requestStartTime 70 | self.initialResponseTime = initialResponseTime 71 | self.requestCompletedTime = requestCompletedTime 72 | self.serializationCompletedTime = serializationCompletedTime 73 | 74 | self.latency = initialResponseTime - requestStartTime 75 | self.requestDuration = requestCompletedTime - requestStartTime 76 | self.serializationDuration = serializationCompletedTime - requestCompletedTime 77 | self.totalDuration = serializationCompletedTime - requestStartTime 78 | } 79 | } 80 | 81 | // MARK: - CustomStringConvertible 82 | 83 | extension Timeline: CustomStringConvertible { 84 | /// The textual representation used when written to an output stream, which includes the latency, the request 85 | /// duration and the total duration. 86 | public var description: String { 87 | let latency = String(format: "%.3f", self.latency) 88 | let requestDuration = String(format: "%.3f", self.requestDuration) 89 | let serializationDuration = String(format: "%.3f", self.serializationDuration) 90 | let totalDuration = String(format: "%.3f", self.totalDuration) 91 | 92 | // NOTE: Had to move to string concatenation due to memory leak filed as rdar://26761490. Once memory leak is 93 | // fixed, we should move back to string interpolation by reverting commit 7d4a43b1. 94 | let timings = [ 95 | "\"Latency\": " + latency + " secs", 96 | "\"Request Duration\": " + requestDuration + " secs", 97 | "\"Serialization Duration\": " + serializationDuration + " secs", 98 | "\"Total Duration\": " + totalDuration + " secs" 99 | ] 100 | 101 | return "Timeline: { " + timings.joined(separator: ", ") + " }" 102 | } 103 | } 104 | 105 | // MARK: - CustomDebugStringConvertible 106 | 107 | extension Timeline: CustomDebugStringConvertible { 108 | /// The textual representation used when written to an output stream, which includes the request start time, the 109 | /// initial response time, the request completed time, the serialization completed time, the latency, the request 110 | /// duration and the total duration. 111 | public var debugDescription: String { 112 | let requestStartTime = String(format: "%.3f", self.requestStartTime) 113 | let initialResponseTime = String(format: "%.3f", self.initialResponseTime) 114 | let requestCompletedTime = String(format: "%.3f", self.requestCompletedTime) 115 | let serializationCompletedTime = String(format: "%.3f", self.serializationCompletedTime) 116 | let latency = String(format: "%.3f", self.latency) 117 | let requestDuration = String(format: "%.3f", self.requestDuration) 118 | let serializationDuration = String(format: "%.3f", self.serializationDuration) 119 | let totalDuration = String(format: "%.3f", self.totalDuration) 120 | 121 | // NOTE: Had to move to string concatenation due to memory leak filed as rdar://26761490. Once memory leak is 122 | // fixed, we should move back to string interpolation by reverting commit 7d4a43b1. 123 | let timings = [ 124 | "\"Request Start Time\": " + requestStartTime, 125 | "\"Initial Response Time\": " + initialResponseTime, 126 | "\"Request Completed Time\": " + requestCompletedTime, 127 | "\"Serialization Completed Time\": " + serializationCompletedTime, 128 | "\"Latency\": " + latency + " secs", 129 | "\"Request Duration\": " + requestDuration + " secs", 130 | "\"Serialization Duration\": " + serializationDuration + " secs", 131 | "\"Total Duration\": " + totalDuration + " secs" 132 | ] 133 | 134 | return "Timeline: { " + timings.joined(separator: ", ") + " }" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Pods/MBProgressHUD/README.mdown: -------------------------------------------------------------------------------- 1 | # MBProgressHUD 2 | 3 | [![Build Status](https://travis-ci.org/matej/MBProgressHUD.svg?branch=master)](https://travis-ci.org/matej/MBProgressHUD) [![codecov.io](https://codecov.io/github/matej/MBProgressHUD/coverage.svg?branch=master)](https://codecov.io/github/matej/MBProgressHUD?branch=master) 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) [![CocoaPods compatible](https://img.shields.io/cocoapods/v/MBProgressHUD.svg?style=flat)](https://cocoapods.org/pods/MBProgressHUD) [![License: MIT](https://img.shields.io/cocoapods/l/MBProgressHUD.svg?style=flat)](http://opensource.org/licenses/MIT) 5 | 6 | `MBProgressHUD` is an iOS drop-in class that displays a translucent HUD with an indicator and/or labels while work is being done in a background thread. The HUD is meant as a replacement for the undocumented, private `UIKit` `UIProgressHUD` with some additional features. 7 | 8 | [![](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/1-thumb.png)](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/1.png) 9 | [![](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/2-thumb.png)](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/2.png) 10 | [![](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/3-thumb.png)](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/3.png) 11 | [![](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/4-thumb.png)](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/4.png) 12 | [![](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/5-thumb.png)](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/5.png) 13 | [![](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/6-thumb.png)](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/6.png) 14 | [![](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/7-thumb.png)](http://dl.dropbox.com/u/378729/MBProgressHUD/v1/7.png) 15 | 16 | **NOTE:** The class has recently undegone a major rewrite. The old version is available in the [legacy](https://github.com/jdg/MBProgressHUD/tree/legacy) branch, should you need it. 17 | 18 | ## Requirements 19 | 20 | `MBProgressHUD` works on iOS 6+ and requires ARC to build. It depends on the following Apple frameworks, which should already be included with most Xcode templates: 21 | 22 | * Foundation.framework 23 | * UIKit.framework 24 | * CoreGraphics.framework 25 | 26 | You will need the latest developer tools in order to build `MBProgressHUD`. Old Xcode versions might work, but compatibility will not be explicitly maintained. 27 | 28 | ## Adding MBProgressHUD to your project 29 | 30 | ### CocoaPods 31 | 32 | [CocoaPods](http://cocoapods.org) is the recommended way to add MBProgressHUD to your project. 33 | 34 | 1. Add a pod entry for MBProgressHUD to your Podfile `pod 'MBProgressHUD', '~> 1.0.0'` 35 | 2. Install the pod(s) by running `pod install`. 36 | 3. Include MBProgressHUD wherever you need it with `#import "MBProgressHUD.h"`. 37 | 38 | ### Carthage 39 | 40 | 1. Add MBProgressHUD to your Cartfile. e.g., `github "jdg/MBProgressHUD" ~> 1.0.0` 41 | 2. Run `carthage update` 42 | 3. Follow the rest of the [standard Carthage installation instructions](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) to add MBProgressHUD to your project. 43 | 44 | ### Source files 45 | 46 | Alternatively you can directly add the `MBProgressHUD.h` and `MBProgressHUD.m` source files to your project. 47 | 48 | 1. Download the [latest code version](https://github.com/matej/MBProgressHUD/archive/master.zip) or add the repository as a git submodule to your git-tracked project. 49 | 2. Open your project in Xcode, then drag and drop `MBProgressHUD.h` and `MBProgressHUD.m` onto your project (use the "Product Navigator view"). Make sure to select Copy items when asked if you extracted the code archive outside of your project. 50 | 3. Include MBProgressHUD wherever you need it with `#import "MBProgressHUD.h"`. 51 | 52 | ### Static library 53 | 54 | You can also add MBProgressHUD as a static library to your project or workspace. 55 | 56 | 1. Download the [latest code version](https://github.com/matej/MBProgressHUD/downloads) or add the repository as a git submodule to your git-tracked project. 57 | 2. Open your project in Xcode, then drag and drop `MBProgressHUD.xcodeproj` onto your project or workspace (use the "Product Navigator view"). 58 | 3. Select your target and go to the Build phases tab. In the Link Binary With Libraries section select the add button. On the sheet find and add `libMBProgressHUD.a`. You might also need to add `MBProgressHUD` to the Target Dependencies list. 59 | 4. Include MBProgressHUD wherever you need it with `#import `. 60 | 61 | ## Usage 62 | 63 | The main guideline you need to follow when dealing with MBProgressHUD while running long-running tasks is keeping the main thread work-free, so the UI can be updated promptly. The recommended way of using MBProgressHUD is therefore to set it up on the main thread and then spinning the task, that you want to perform, off onto a new thread. 64 | 65 | ```objective-c 66 | [MBProgressHUD showHUDAddedTo:self.view animated:YES]; 67 | dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ 68 | // Do something... 69 | dispatch_async(dispatch_get_main_queue(), ^{ 70 | [MBProgressHUD hideHUDForView:self.view animated:YES]; 71 | }); 72 | }); 73 | ``` 74 | 75 | You can add the HUD on any view or window. It is however a good idea to avoid adding the HUD to certain `UIKit` views with complex view hierarchies - like `UITableView` or `UICollectionView`. Those can mutate their subviews in unexpected ways and thereby break HUD display. 76 | 77 | If you need to configure the HUD you can do this by using the MBProgressHUD reference that showHUDAddedTo:animated: returns. 78 | 79 | ```objective-c 80 | MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; 81 | hud.mode = MBProgressHUDModeAnnularDeterminate; 82 | hud.labelText = @"Loading"; 83 | [self doSomethingInBackgroundWithProgressCallback:^(float progress) { 84 | hud.progress = progress; 85 | } completionCallback:^{ 86 | [hud hide:YES]; 87 | }]; 88 | ``` 89 | 90 | You can also use a `NSProgress` object and MBProgressHUD will update itself when there is progress reported through that object. 91 | 92 | ```objective-c 93 | MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; 94 | hud.mode = MBProgressHUDModeAnnularDeterminate; 95 | hud.labelText = @"Loading"; 96 | NSProgress *progress = [self doSomethingInBackgroundCompletion:^{ 97 | [hud hide:YES]; 98 | }]; 99 | hud.progressObject = progress; 100 | ``` 101 | 102 | UI updates should always be done on the main thread. Some MBProgressHUD setters are however considered "thread safe" and can be called from background threads. Those also include `setMode:`, `setCustomView:`, `setLabelText:`, `setLabelFont:`, `setDetailsLabelText:`, `setDetailsLabelFont:` and `setProgress:`. 103 | 104 | If you need to run your long-running task in the main thread, you should perform it with a slight delay, so UIKit will have enough time to update the UI (i.e., draw the HUD) before you block the main thread with your task. 105 | 106 | ```objective-c 107 | [MBProgressHUD showHUDAddedTo:self.view animated:YES]; 108 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC); 109 | dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 110 | // Do something... 111 | [MBProgressHUD hideHUDForView:self.view animated:YES]; 112 | }); 113 | ``` 114 | 115 | You should be aware that any HUD updates issued inside the above block won't be displayed until the block completes. 116 | 117 | For more examples, including how to use MBProgressHUD with asynchronous operations such as NSURLConnection, take a look at the bundled demo project. Extensive API documentation is provided in the header file (MBProgressHUD.h). 118 | 119 | 120 | ## License 121 | 122 | This code is distributed under the terms and conditions of the [MIT license](LICENSE). 123 | 124 | ## Change-log 125 | 126 | A brief summary of each MBProgressHUD release can be found in the [CHANGELOG](CHANGELOG.mdown). 127 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MoviesApp/Pods-MoviesApp-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright (c) 2014-2017 Alamofire Software Foundation (http://alamofire.org/) 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | 37 | License 38 | MIT 39 | Title 40 | Alamofire 41 | Type 42 | PSGroupSpecifier 43 | 44 | 45 | FooterText 46 | Copyright © 2009-2016 Matej Bukovinski 47 | 48 | Permission is hereby granted, free of charge, to any person obtaining a copy 49 | of this software and associated documentation files (the "Software"), to deal 50 | in the Software without restriction, including without limitation the rights 51 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 52 | copies of the Software, and to permit persons to whom the Software is 53 | furnished to do so, subject to the following conditions: 54 | 55 | The above copyright notice and this permission notice shall be included in 56 | all copies or substantial portions of the Software. 57 | 58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 59 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 60 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 61 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 62 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 63 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 64 | THE SOFTWARE. 65 | License 66 | MIT 67 | Title 68 | MBProgressHUD 69 | Type 70 | PSGroupSpecifier 71 | 72 | 73 | FooterText 74 | The MIT License (MIT) 75 | 76 | Copyright (c) 2015-2016 Radosław Pietruszewski 77 | 78 | Permission is hereby granted, free of charge, to any person obtaining a copy 79 | of this software and associated documentation files (the "Software"), to deal 80 | in the Software without restriction, including without limitation the rights 81 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 82 | copies of the Software, and to permit persons to whom the Software is 83 | furnished to do so, subject to the following conditions: 84 | 85 | The above copyright notice and this permission notice shall be included in all 86 | copies or substantial portions of the Software. 87 | 88 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 89 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 90 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 91 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 92 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 93 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 94 | SOFTWARE. 95 | 96 | License 97 | MIT 98 | Title 99 | SwiftyUserDefaults 100 | Type 101 | PSGroupSpecifier 102 | 103 | 104 | FooterText 105 | Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov 106 | 107 | Permission is hereby granted, free of charge, to any person obtaining a copy 108 | of this software and associated documentation files (the "Software"), to deal 109 | in the Software without restriction, including without limitation the rights 110 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 111 | copies of the Software, and to permit persons to whom the Software is 112 | furnished to do so, subject to the following conditions: 113 | 114 | The above copyright notice and this permission notice shall be included in 115 | all copies or substantial portions of the Software. 116 | 117 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 118 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 119 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 120 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 121 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 122 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 123 | THE SOFTWARE. 124 | License 125 | MIT 126 | Title 127 | TableKit 128 | Type 129 | PSGroupSpecifier 130 | 131 | 132 | FooterText 133 | The MIT License (MIT) 134 | 135 | Copyright (c) 2015 John Sundell 136 | 137 | Permission is hereby granted, free of charge, to any person obtaining a copy 138 | of this software and associated documentation files (the "Software"), to deal 139 | in the Software without restriction, including without limitation the rights 140 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 141 | copies of the Software, and to permit persons to whom the Software is 142 | furnished to do so, subject to the following conditions: 143 | 144 | The above copyright notice and this permission notice shall be included in all 145 | copies or substantial portions of the Software. 146 | 147 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 148 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 149 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 150 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 151 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 152 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 153 | SOFTWARE. 154 | 155 | 156 | License 157 | MIT 158 | Title 159 | Unbox 160 | Type 161 | PSGroupSpecifier 162 | 163 | 164 | FooterText 165 | Generated by CocoaPods - https://cocoapods.org 166 | Title 167 | 168 | Type 169 | PSGroupSpecifier 170 | 171 | 172 | StringsTable 173 | Acknowledgements 174 | Title 175 | Acknowledgements 176 | 177 | 178 | -------------------------------------------------------------------------------- /Pods/TableKit/README.md: -------------------------------------------------------------------------------- 1 | # TableKit 2 | 3 |

4 | Build Status 5 | Swift 4.0 compatible 6 | Carthage compatible 7 | CocoaPods compatible 8 | Platform iOS 9 | License: MIT 10 |

11 | 12 | TableKit is a super lightweight yet powerful generic library that allows you to build complex table views in a declarative type-safe manner. 13 | It hides a complexity of `UITableViewDataSource` and `UITableViewDelegate` methods behind the scene, so your code will be look clean, easy to read and nice to maintain. 14 | 15 | # Features 16 | 17 | - [x] Type-safe generic cells 18 | - [x] Functional programming style friendly 19 | - [x] The easiest way to map your models or view models to cells 20 | - [x] Automatic cell registration* 21 | - [x] Correctly handles autolayout cells with multiline labels 22 | - [x] Chainable cell actions (select/deselect etc.) 23 | - [x] Support cells created from code, xib, or storyboard 24 | - [x] Support different cells height calculation strategies 25 | - [x] Support portrait and landscape orientations 26 | - [x] No need to subclass 27 | - [x] Extensibility 28 | 29 | # Getting Started 30 | 31 | An [example app](Demo) is included demonstrating TableKit's functionality. 32 | 33 | ## Basic usage 34 | 35 | Create your rows: 36 | ```swift 37 | import TableKit 38 | 39 | let row1 = TableRow(item: "1") 40 | let row2 = TableRow(item: 2) 41 | let row3 = TableRow(item: User(name: "John Doe", rating: 5)) 42 | ``` 43 | Put rows into section: 44 | ```swift 45 | let section = TableSection(rows: [row1, row2, row3]) 46 | ``` 47 | And setup your table: 48 | ```swift 49 | let tableDirector = TableDirector(tableView: tableView) 50 | tableDirector += section 51 | ``` 52 | Done. Your table is ready. Your cells have to conform to `ConfigurableCell` protocol: 53 | ```swift 54 | class StringTableViewCell: UITableViewCell, ConfigurableCell { 55 | 56 | func configure(with string: String) { 57 | 58 | textLabel?.text = string 59 | } 60 | } 61 | 62 | class UserTableViewCell: UITableViewCell, ConfigurableCell { 63 | 64 | static var estimatedHeight: CGFloat? { 65 | return 100 66 | } 67 | 68 | // is not required to be implemented 69 | // by default reuse id is equal to cell's class name 70 | static var reuseIdentifier: String { 71 | return "my id" 72 | } 73 | 74 | func configure(with user: User) { 75 | 76 | textLabel?.text = user.name 77 | detailTextLabel?.text = "Rating: \(user.rating)" 78 | } 79 | } 80 | ``` 81 | You could have as many rows and sections as you need. 82 | 83 | ## Row actions 84 | 85 | It nice to have some actions that related to your cells: 86 | ```swift 87 | let action = TableRowAction(.click) { (options) in 88 | 89 | // you could access any useful information that relates to the action 90 | 91 | // options.cell - StringTableViewCell? 92 | // options.item - String 93 | // options.indexPath - IndexPath 94 | // options.userInfo - [AnyHashable: Any]? 95 | } 96 | 97 | let row = TableRow(item: "some", actions: [action]) 98 | ``` 99 | Or, using nice chaining approach: 100 | ```swift 101 | let row = TableRow(item: "some") 102 | .on(.click) { (options) in 103 | 104 | } 105 | .on(.shouldHighlight) { (options) -> Bool in 106 | return false 107 | } 108 | ``` 109 | You could find all available actions [here](Sources/TableRowAction.swift). 110 | 111 | ## Custom row actions 112 | 113 | You are able to define your own actions: 114 | ```swift 115 | struct MyActions { 116 | 117 | static let ButtonClicked = "ButtonClicked" 118 | } 119 | 120 | class MyTableViewCell: UITableViewCell, ConfigurableCell { 121 | 122 | @IBAction func myButtonClicked(sender: UIButton) { 123 | 124 | TableCellAction(key: MyActions.ButtonClicked, sender: self).invoke() 125 | } 126 | } 127 | ``` 128 | And handle them accordingly: 129 | ```swift 130 | let myAction = TableRowAction(.custom(MyActions.ButtonClicked)) { (options) in 131 | 132 | } 133 | ``` 134 | ## Multiple actions with same type 135 | 136 | It's also possible to use multiple actions with same type: 137 | ```swift 138 | let click1 = TableRowAction(.click) { (options) in } 139 | click1.id = "click1" // optional 140 | 141 | let click2 = TableRowAction(.click) { (options) in } 142 | click2.id = "click2" // optional 143 | 144 | let row = TableRow(item: "some", actions: [click1, click2]) 145 | ``` 146 | Could be useful in case if you want to separate your logic somehow. Actions will be invoked in order which they were attached. 147 | > If you define multiple actions with same type which also return a value, only last return value will be used for table view. 148 | 149 | You could also remove any action by id: 150 | ```swift 151 | row.removeAction(forActionId: "action_id") 152 | ``` 153 | 154 | # Advanced 155 | 156 | ## Cell height calculating strategy 157 | By default TableKit relies on self-sizing cells. In that case you have to provide an estimated height for your cells: 158 | ```swift 159 | class StringTableViewCell: UITableViewCell, ConfigurableCell { 160 | 161 | // ... 162 | 163 | static var estimatedHeight: CGFloat? { 164 | return 255 165 | } 166 | } 167 | ``` 168 | It's enough for most cases. But you may be not happy with this. So you could use a prototype cell to calculate cells heights. To enable this feature simply use this property: 169 | ```swift 170 | let tableDirector = TableDirector(tableView: tableView, shouldUsePrototypeCellHeightCalculation: true) 171 | ``` 172 | It does all dirty work with prototypes for you [behind the scene](Sources/TablePrototypeCellHeightCalculator.swift), so you don't have to worry about anything except of your cell configuration: 173 | ```swift 174 | class ImageTableViewCell: UITableViewCell, ConfigurableCell { 175 | 176 | func configure(with url: NSURL) { 177 | 178 | loadImageAsync(url: url, imageView: imageView) 179 | } 180 | 181 | override func layoutSubviews() { 182 | super.layoutSubviews() 183 | 184 | contentView.layoutIfNeeded() 185 | multilineLabel.preferredMaxLayoutWidth = multilineLabel.bounds.size.width 186 | } 187 | } 188 | ``` 189 | You have to additionally set `preferredMaxLayoutWidth` for all your multiline labels. 190 | 191 | ## Functional programming 192 | It's never been so easy to deal with table views. 193 | ```swift 194 | let users = /* some users array */ 195 | 196 | let click = TableRowAction(.click) { 197 | 198 | } 199 | 200 | let rows = users.filter({ $0.state == .active }).map({ TableRow(item: $0.name, actions: [click]) }) 201 | 202 | tableDirector += rows 203 | ``` 204 | Done, your table is ready. 205 | ## Automatic cell registration 206 | 207 | TableKit can register your cells in a table view automatically. In case if your reusable cell id mathces cell's xib name: 208 | 209 | ```ruby 210 | MyTableViewCell.swift 211 | MyTableViewCell.xib 212 | 213 | ``` 214 | You can also turn off this behaviour: 215 | ```swift 216 | let tableDirector = TableDirector(tableView: tableView, shouldUseAutomaticCellRegistration: false) 217 | ``` 218 | and register your cell manually. 219 | 220 | # Installation 221 | 222 | ## CocoaPods 223 | To integrate TableKit into your Xcode project using CocoaPods, specify it in your `Podfile`: 224 | 225 | ```ruby 226 | pod 'TableKit' 227 | ``` 228 | ## Carthage 229 | Add the line `github "maxsokolov/tablekit"` to your `Cartfile`. 230 | ## Manual 231 | Clone the repo and drag files from `Sources` folder into your Xcode project. 232 | 233 | # Requirements 234 | 235 | - iOS 8.0 236 | - Xcode 9.0 237 | 238 | # Changelog 239 | 240 | Keep eye on [changes](CHANGELOG.md). 241 | 242 | # License 243 | 244 | TableKit is available under the MIT license. See LICENSE for details. 245 | -------------------------------------------------------------------------------- /MoviesApp/Features/MovieList/MovieListScreen.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 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | --------------------------------------------------------------------------------