├── .codebeatignore
├── .travis.yml
├── DADependencyInjection.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcshareddata
│ └── xcschemes
│ │ └── DADependencyInjection.xcscheme
└── xcuserdata
│ └── dejan.xcuserdatad
│ ├── xcdebugger
│ └── Breakpoints_v2.xcbkptlist
│ └── xcschemes
│ ├── SiriTest.xcscheme
│ └── xcschememanagement.plist
├── DADependencyInjection
├── AppDelegate.swift
├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Core
│ ├── Data
│ │ ├── DataProvider
│ │ │ ├── MoviesDataProvider.swift
│ │ │ ├── MoviesDataSource.swift
│ │ │ └── MoviesDataSource_Operations.swift
│ │ ├── Factory
│ │ │ ├── JSONMoviesFactory.swift
│ │ │ └── MoviesFactoryProvider.swift
│ │ ├── ListDisplayable.swift
│ │ ├── Manager
│ │ │ ├── ListDisplayableDataProvider.swift
│ │ │ └── MoviesManager.swift
│ │ └── MovieItem.swift
│ ├── Networking
│ │ ├── AFNetworkConnector.swift
│ │ ├── NSURLNetworkConnector.swift
│ │ └── NetworkingProvider.swift
│ └── Operations
│ │ ├── DAOperation.swift
│ │ ├── GetDataOperation.swift
│ │ └── ParseDataOperation.swift
├── DADependencyInjection.entitlements
├── DetailsViewController.swift
├── Info.plist
├── Libraries
│ └── Alamofire
│ │ ├── AFError.swift
│ │ ├── Alamofire.h
│ │ ├── Alamofire.swift
│ │ ├── DispatchQueue+Alamofire.swift
│ │ ├── MultipartFormData.swift
│ │ ├── NetworkReachabilityManager.swift
│ │ ├── Notifications.swift
│ │ ├── ParameterEncoding.swift
│ │ ├── Request.swift
│ │ ├── Response.swift
│ │ ├── ResponseSerialization.swift
│ │ ├── Result.swift
│ │ ├── ServerTrustPolicy.swift
│ │ ├── SessionDelegate.swift
│ │ ├── SessionManager.swift
│ │ ├── TaskDelegate.swift
│ │ ├── Timeline.swift
│ │ └── Validation.swift
├── SiriConstants.swift
└── ViewController.swift
├── DADependencyInjectionTests
├── Data
│ ├── TestData.swift
│ ├── TestFileNames.swift
│ └── popularMovies.json
├── Info.plist
├── MockObjects
│ └── MockNetworkProvider.swift
├── MoviesDataSourceTests.swift
├── MoviesFactoryTests.swift
└── MoviesManagerTests.swift
├── README.md
├── SiriTest
├── Info.plist
└── IntentHandler.swift
└── codecov.yml
/.codebeatignore:
--------------------------------------------------------------------------------
1 | DADependencyInjection/Libraries/**
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: swift
2 | osx_image: xcode8.3
3 | script:
4 | - xcodebuild test -project DADependencyInjection.xcodeproj -scheme DADependencyInjection -destination 'platform=iOS Simulator,name=iPhone 7,OS=10.3.1'
5 | after_success:
6 | - bash <(curl -s https://codecov.io/bash)
--------------------------------------------------------------------------------
/DADependencyInjection.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/DADependencyInjection.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/DADependencyInjection.xcodeproj/xcshareddata/xcschemes/DADependencyInjection.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
65 |
67 |
73 |
74 |
75 |
76 |
77 |
78 |
84 |
86 |
92 |
93 |
94 |
95 |
97 |
98 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/DADependencyInjection.xcodeproj/xcuserdata/dejan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
8 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/DADependencyInjection.xcodeproj/xcuserdata/dejan.xcuserdatad/xcschemes/SiriTest.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
16 |
22 |
23 |
24 |
30 |
36 |
37 |
38 |
39 |
40 |
45 |
46 |
47 |
48 |
54 |
55 |
56 |
57 |
58 |
59 |
70 |
72 |
78 |
79 |
80 |
81 |
82 |
83 |
90 |
92 |
98 |
99 |
100 |
101 |
103 |
104 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/DADependencyInjection.xcodeproj/xcuserdata/dejan.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | DADependencyInjection.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | SiriTest.xcscheme
13 |
14 | orderHint
15 | 1
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | 7757D2341E5A627400C23217
21 |
22 | primary
23 |
24 |
25 | 7757D2481E5A627400C23217
26 |
27 | primary
28 |
29 |
30 | 77A3A0771ECD1299009363DC
31 |
32 | primary
33 |
34 |
35 | 77A3A0801ECD129A009363DC
36 |
37 | primary
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/DADependencyInjection/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 19/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
17 | return true
18 | }
19 |
20 | func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
21 |
22 | if let title = userActivity.userInfo?[SiriConstants.ItemTitle.rawValue] as? String,
23 | let desc = userActivity.userInfo?[SiriConstants.ItemDescription.rawValue] as? String {
24 |
25 | if let detailsVC = self.window?.rootViewController?.storyboard?.instantiateViewController(withIdentifier: "DetailsVC") as? DetailsViewController {
26 | detailsVC.itemTitle = title
27 | detailsVC.itemDescription = desc
28 |
29 | self.window?.rootViewController?.present(detailsVC, animated: true, completion: nil)
30 | }
31 | }
32 |
33 | return true
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/DADependencyInjection/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "ipad",
35 | "size" : "29x29",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "ipad",
40 | "size" : "29x29",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "40x40",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "40x40",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "76x76",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "76x76",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/DADependencyInjection/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 |
--------------------------------------------------------------------------------
/DADependencyInjection/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
55 |
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 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
117 |
126 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Data/DataProvider/MoviesDataProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MoviesDataProvider.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 21/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol MoviesDataProvider {
12 | var networkingProvider: NetworkingProvider { get set }
13 | var moviesFactory: MoviesFactoryProvider { get set }
14 | func getMovies(onCompleted: (([MovieItem]) -> ())?)
15 | }
16 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Data/DataProvider/MoviesDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MoviesDataSource.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 21/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | struct DataSourceConstants {
12 | static let TMDBBaseURL = "https://api.themoviedb.org"
13 | static let MoviesURL = "/3/movie/popular"
14 | static let APIParameterKey = "api_key"
15 | static let APIParameterValue = "_YOUR_API_KEY_HERE_"
16 | static let PageParameterKey = "page"
17 |
18 | static func URLString(forPage page: String = "1") -> String? {
19 |
20 | let urlComponents = NSURLComponents(string: DataSourceConstants.TMDBBaseURL)
21 | urlComponents?.path = DataSourceConstants.MoviesURL
22 |
23 | let apiKey = URLQueryItem(name: DataSourceConstants.APIParameterKey, value: DataSourceConstants.APIParameterValue)
24 | let page = URLQueryItem(name: DataSourceConstants.PageParameterKey, value: page)
25 |
26 | urlComponents?.queryItems = [apiKey, page]
27 |
28 | return urlComponents?.string
29 | }
30 | }
31 |
32 | class MoviesDataSource: MoviesDataProvider {
33 |
34 | public var networkingProvider: NetworkingProvider
35 | public var moviesFactory: MoviesFactoryProvider
36 |
37 | public init(withNetworkingProvider networking: NetworkingProvider = AFNetworkConnector(), andFactory factory: MoviesFactoryProvider = JSONMoviesFactory()) {
38 | self.networkingProvider = networking
39 | self.moviesFactory = factory
40 | }
41 |
42 |
43 | func getMovies(onCompleted: (([MovieItem]) -> ())?) {
44 |
45 | var result: [MovieItem] = []
46 |
47 | let dispatchGroup = DispatchGroup()
48 |
49 | for i in 1...5 {
50 | guard
51 | let urlString = DataSourceConstants.URLString(forPage: "\(i)")
52 | else {
53 | continue
54 | }
55 |
56 | dispatchGroup.enter()
57 | self.networkingProvider.restCall(urlString: urlString) {
58 |
59 | (responseObject) in
60 |
61 | guard
62 | let responseData = responseObject,
63 | let jsonObject = try? JSONSerialization.jsonObject(with: responseData, options: [JSONSerialization.ReadingOptions.allowFragments])
64 | else {
65 | dispatchGroup.leave()
66 | return
67 | }
68 |
69 | let movies = self.moviesFactory.movieItems(withJSON: jsonObject)
70 | result = result + movies
71 |
72 | dispatchGroup.leave()
73 | }
74 | }
75 |
76 | dispatchGroup.notify(queue: DispatchQueue.global()) {
77 | onCompleted?(result)
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Data/DataProvider/MoviesDataSource_Operations.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MoviesDataSource_Operations.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 28/07/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class MoviesDataSource_Operations: MoviesDataProvider {
12 |
13 | public var networkingProvider: NetworkingProvider
14 | public var moviesFactory: MoviesFactoryProvider
15 |
16 | private let operationQueue: OperationQueue = OperationQueue()
17 |
18 | public init(withNetworkingProvider networking: NetworkingProvider = AFNetworkConnector(), andFactory factory: MoviesFactoryProvider = JSONMoviesFactory()) {
19 | self.networkingProvider = networking
20 | self.moviesFactory = factory
21 | self.operationQueue.maxConcurrentOperationCount = 5
22 | }
23 |
24 | func getMovies(onCompleted: (([MovieItem]) -> ())?) {
25 |
26 | operationQueue.cancelAllOperations()
27 |
28 | var result: [MovieItem] = []
29 |
30 | let queueCompletionOperation = BlockOperation {
31 | onCompleted?(result)
32 | }
33 |
34 | var operations: [Operation] = []
35 |
36 | operationQueue.isSuspended = true
37 |
38 | for i in 1...5 {
39 | guard
40 | let urlString = DataSourceConstants.URLString(forPage: "\(i)")
41 | else {
42 | continue
43 | }
44 |
45 | let networkingOperation = GetDataOperation(withURLString: urlString, andNetworkingProvider: networkingProvider)
46 | let parsingOperation = ParseDataOperation(withFactory: moviesFactory)
47 |
48 | networkingOperation.completionBlock = {
49 | parsingOperation.moviesData = networkingOperation.responseData
50 | }
51 |
52 | parsingOperation.completionBlock = {
53 | if let moviesArray = parsingOperation.movies {
54 | DispatchQueue.global().sync(flags: .barrier) {
55 | result = result + moviesArray
56 | }
57 | }
58 | }
59 |
60 | parsingOperation.addDependency(networkingOperation)
61 |
62 | queueCompletionOperation.addDependency(parsingOperation)
63 |
64 | operations.append(contentsOf: [parsingOperation, networkingOperation])
65 | }
66 |
67 | operations.append(queueCompletionOperation)
68 |
69 | operationQueue.addOperations(operations, waitUntilFinished: false)
70 | operationQueue.isSuspended = false
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Data/Factory/JSONMoviesFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MoviesFactory.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 22/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | private struct MovieItemKey {
12 | static let Results = "results"
13 | static let ID = "id"
14 | static let Title = "title"
15 | static let Description = "overview"
16 | static let AverageVote = "vote_average"
17 | }
18 |
19 | class JSONMoviesFactory: MoviesFactoryProvider {
20 |
21 | private func movieItem(withJSON json: Any) -> MovieItem? {
22 |
23 | guard
24 | let jsonDict = json as? [String: Any?],
25 | let id = jsonDict[MovieItemKey.ID] as? Int,
26 | let title = jsonDict[MovieItemKey.Title] as? String,
27 | let description = jsonDict[MovieItemKey.Description] as? String,
28 | let vote = jsonDict[MovieItemKey.AverageVote] as? Double
29 | else {
30 | return nil
31 | }
32 |
33 | return MovieFactoryItem(id: id, title: title, movieDescription: description, averageVote: vote)
34 | }
35 |
36 | func movieItems(withJSON json: Any) -> [MovieItem] {
37 |
38 | guard
39 | let jsonDict = json as? [String: Any?],
40 | let arrayDict = jsonDict[MovieItemKey.Results] as? [[String: Any?]]
41 | else {
42 | return []
43 | }
44 |
45 | return arrayDict.flatMap { movieItem(withJSON: $0) }
46 | }
47 | }
48 |
49 | private struct MovieFactoryItem: MovieItem {
50 | let id: Int
51 | let title: String
52 | let movieDescription: String
53 | let averageVote: Double
54 | }
55 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Data/Factory/MoviesFactoryProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MoviesFactoryProtocol.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 22/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol MoviesFactoryProvider {
12 | func movieItems(withJSON json: Any) -> [MovieItem]
13 | }
14 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Data/ListDisplayable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListDisplayable.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 22/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol ListDisplayable {
12 | var listItemTitle: String { get }
13 | var listItemSubtitle: String? { get }
14 | }
15 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Data/Manager/ListDisplayableDataProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListDisplayableDataProvider.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 23/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol ListDisplayableDataProvider {
12 | func getListItems(onCompleted: (([ListDisplayable]) -> ())?)
13 | func searchListItems(searchTerm: String, onCompleted: (([ListDisplayable]) -> ())?)
14 | }
15 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Data/Manager/MoviesManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MoviesManager.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 22/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class MoviesManager: ListDisplayableDataProvider {
12 |
13 | private var cachedItems: [ListDisplayable]?
14 |
15 | var moviesDataProvider: MoviesDataProvider
16 |
17 | init(withDataProvider dataProvider: MoviesDataProvider = MoviesDataSource()) {
18 | self.moviesDataProvider = dataProvider
19 | }
20 |
21 | func getListItems(onCompleted: (([ListDisplayable]) -> ())?) {
22 | self.moviesDataProvider.getMovies {
23 |
24 | (movies) in
25 |
26 | let listItems = movies.map({ ListDisplayableItem(withMovieItem: $0) })
27 |
28 | DispatchQueue.main.async(execute: {
29 | onCompleted?(listItems)
30 | })
31 |
32 | self.cachedItems = listItems
33 | }
34 | }
35 |
36 | // TODO: Save first movie
37 |
38 | func searchListItems(searchTerm: String, onCompleted: (([ListDisplayable]) -> ())?) {
39 |
40 | if let cached = cachedItems {
41 | onCompleted?(self.filter(cached, term: searchTerm))
42 | } else {
43 | getListItems(onCompleted: { (items) in
44 | onCompleted?(self.filter(items, term: searchTerm))
45 | })
46 | }
47 | }
48 |
49 | private func filter(_ list: [ListDisplayable], term: String) -> [ListDisplayable] {
50 | return list.filter(){ $0.listItemTitle.lowercased().contains(term.lowercased()) }
51 | }
52 | }
53 |
54 | // MARK: ListDisplayable
55 | private struct ListDisplayableItem: ListDisplayable {
56 | var listItemTitle: String
57 | var listItemSubtitle: String?
58 |
59 | init(withMovieItem item: MovieItem) {
60 | self.listItemTitle = item.title
61 | self.listItemSubtitle = item.movieDescription
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Data/MovieItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MovieItem.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 21/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol MovieItem {
12 | var id: Int { get }
13 | var title: String { get }
14 | var movieDescription: String { get }
15 | var averageVote: Double { get }
16 | }
17 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Networking/AFNetworkConnector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AFNetworkConnector.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 20/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class AFNetworkConnector: NetworkingProvider {
12 |
13 | func restCall(urlString: String, onCompleted: ((Data?) -> ())?) {
14 |
15 | guard
16 | let url = try? urlString.asURL()
17 | else {
18 | onCompleted?(nil)
19 | return
20 | }
21 |
22 | SessionManager.default.request(url).responseData {
23 | response in
24 | onCompleted?(response.result.value)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Networking/NSURLNetworkConnector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSURLNetworkConnector.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 23/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class NSURLNetworkConnector: NetworkingProvider {
12 |
13 | func restCall(urlString: String, onCompleted: ((Data?) -> ())?) {
14 |
15 | guard
16 | let url = try? urlString.asURL()
17 | else {
18 | onCompleted?(nil)
19 | return
20 | }
21 |
22 | let urlTask = URLSession.shared.dataTask(with: url) {
23 | (data, response, error) in
24 | onCompleted?(data)
25 | }
26 |
27 | urlTask.resume()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Networking/NetworkingProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkConnectable.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 21/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol NetworkingProvider {
12 | func restCall(urlString: String, onCompleted: ((Data?) -> ())?)
13 | }
14 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Operations/DAOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DAOperation.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 28/07/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class DAOperation: Operation {
12 |
13 | private var _executing = false {
14 | willSet {
15 | willChangeValue(forKey: "isExecuting")
16 | }
17 | didSet {
18 | didChangeValue(forKey: "isExecuting")
19 | }
20 | }
21 |
22 | override var isExecuting: Bool {
23 | return _executing
24 | }
25 |
26 | private var _finished = false {
27 | willSet {
28 | willChangeValue(forKey: "isFinished")
29 | }
30 |
31 | didSet {
32 | didChangeValue(forKey: "isFinished")
33 | }
34 | }
35 |
36 | override var isFinished: Bool {
37 | return _finished
38 | }
39 |
40 | func executing(_ executing: Bool) {
41 | _executing = executing
42 | }
43 |
44 | func finish(_ finished: Bool) {
45 | _finished = finished
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Operations/GetDataOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GetDataOperation.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 28/07/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class GetDataOperation: DAOperation {
12 |
13 | private let urlString: String
14 | private let provider: NetworkingProvider
15 |
16 | var responseData: Data?
17 |
18 | init(withURLString urlString: String, andNetworkingProvider provider: NetworkingProvider = AFNetworkConnector()) {
19 | self.urlString = urlString
20 | self.provider = provider
21 | }
22 |
23 | override func main() {
24 | guard isCancelled == false else {
25 | finish(true)
26 | return
27 | }
28 |
29 | executing(true)
30 | provider.restCall(urlString: urlString) { (data) in
31 | self.responseData = data
32 | self.executing(false)
33 | self.finish(true)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/DADependencyInjection/Core/Operations/ParseDataOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParseDataOperation.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 28/07/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class ParseDataOperation: DAOperation {
12 |
13 | private let factory: MoviesFactoryProvider
14 |
15 | var moviesData: Data?
16 | var movies: [MovieItem]?
17 |
18 | init(withFactory factory: MoviesFactoryProvider = JSONMoviesFactory()) {
19 | self.factory = factory
20 | }
21 |
22 | override func main() {
23 | guard isCancelled == false else {
24 | finish(true)
25 | return
26 | }
27 |
28 | executing(true)
29 | guard
30 | let data = moviesData,
31 | let jsonObject = try? JSONSerialization.jsonObject(with: data, options: [JSONSerialization.ReadingOptions.allowFragments])
32 | else {
33 | executing(false)
34 | finish(true)
35 | return
36 | }
37 |
38 | movies = factory.movieItems(withJSON: jsonObject)
39 |
40 | executing(false)
41 | finish(true)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/DADependencyInjection/DADependencyInjection.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.siri
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/DADependencyInjection/DetailsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailsViewController.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 21/05/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class DetailsViewController: UIViewController {
12 |
13 | public var itemTitle: String?
14 | public var itemDescription: String?
15 |
16 | @IBOutlet weak var titleLabel: UILabel!
17 | @IBOutlet weak var detailsLabel: UILabel!
18 |
19 | override func viewDidLoad() {
20 | super.viewDidLoad()
21 |
22 | self.titleLabel.text = itemTitle
23 | self.detailsLabel.text = itemDescription
24 | }
25 |
26 | @IBAction func dismiss() {
27 | self.dismiss(animated: true, completion: nil)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/DADependencyInjection/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Movies Browser
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | NSSiriUsageDescription
26 | Search for movies using Siri.
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UIRequiredDeviceCapabilities
32 |
33 | armv7
34 |
35 | UISupportedInterfaceOrientations
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationLandscapeLeft
39 | UIInterfaceOrientationLandscapeRight
40 |
41 | UISupportedInterfaceOrientations~ipad
42 |
43 | UIInterfaceOrientationPortrait
44 | UIInterfaceOrientationPortraitUpsideDown
45 | UIInterfaceOrientationLandscapeLeft
46 | UIInterfaceOrientationLandscapeRight
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/Alamofire.h:
--------------------------------------------------------------------------------
1 | //
2 | // Alamofire.h
3 | //
4 | // Copyright (c) 2014-2016 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 | FOUNDATION_EXPORT double AlamofireVersionNumber;
28 | FOUNDATION_EXPORT const unsigned char AlamofireVersionString[];
29 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/Alamofire.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Alamofire.swift
3 | //
4 | // Copyright (c) 2014-2016 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 | /// Types adopting the `URLConvertible` protocol can be used to construct URLs, which are then used to construct
28 | /// URL requests.
29 | public protocol URLConvertible {
30 | /// Returns a URL that conforms to RFC 2396 or throws an `Error`.
31 | ///
32 | /// - throws: An `Error` if the type cannot be converted to a `URL`.
33 | ///
34 | /// - returns: A URL or throws an `Error`.
35 | func asURL() throws -> URL
36 | }
37 |
38 | extension String: URLConvertible {
39 | /// Returns a URL if `self` represents a valid URL string that conforms to RFC 2396 or throws an `AFError`.
40 | ///
41 | /// - throws: An `AFError.invalidURL` if `self` is not a valid URL string.
42 | ///
43 | /// - returns: A URL or throws an `AFError`.
44 | public func asURL() throws -> URL {
45 | guard let url = URL(string: self) else { throw AFError.invalidURL(url: self) }
46 | return url
47 | }
48 | }
49 |
50 | extension URL: URLConvertible {
51 | /// Returns self.
52 | public func asURL() throws -> URL { return self }
53 | }
54 |
55 | extension URLComponents: URLConvertible {
56 | /// Returns a URL if `url` is not nil, otherise throws an `Error`.
57 | ///
58 | /// - throws: An `AFError.invalidURL` if `url` is `nil`.
59 | ///
60 | /// - returns: A URL or throws an `AFError`.
61 | public func asURL() throws -> URL {
62 | guard let url = url else { throw AFError.invalidURL(url: self) }
63 | return url
64 | }
65 | }
66 |
67 | // MARK: -
68 |
69 | /// Types adopting the `URLRequestConvertible` protocol can be used to construct URL requests.
70 | public protocol URLRequestConvertible {
71 | /// Returns a URL request or throws if an `Error` was encountered.
72 | ///
73 | /// - throws: An `Error` if the underlying `URLRequest` is `nil`.
74 | ///
75 | /// - returns: A URL request.
76 | func asURLRequest() throws -> URLRequest
77 | }
78 |
79 | extension URLRequestConvertible {
80 | /// The URL request.
81 | public var urlRequest: URLRequest? { return try? asURLRequest() }
82 | }
83 |
84 | extension URLRequest: URLRequestConvertible {
85 | /// Returns a URL request or throws if an `Error` was encountered.
86 | public func asURLRequest() throws -> URLRequest { return self }
87 | }
88 |
89 | // MARK: -
90 |
91 | extension URLRequest {
92 | /// Creates an instance with the specified `method`, `urlString` and `headers`.
93 | ///
94 | /// - parameter url: The URL.
95 | /// - parameter method: The HTTP method.
96 | /// - parameter headers: The HTTP headers. `nil` by default.
97 | ///
98 | /// - returns: The new `URLRequest` instance.
99 | public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws {
100 | let url = try url.asURL()
101 |
102 | self.init(url: url)
103 |
104 | httpMethod = method.rawValue
105 |
106 | if let headers = headers {
107 | for (headerField, headerValue) in headers {
108 | setValue(headerValue, forHTTPHeaderField: headerField)
109 | }
110 | }
111 | }
112 |
113 | func adapt(using adapter: RequestAdapter?) throws -> URLRequest {
114 | guard let adapter = adapter else { return self }
115 | return try adapter.adapt(self)
116 | }
117 | }
118 |
119 | // MARK: - Data Request
120 |
121 | /// Creates a `DataRequest` using the default `SessionManager` to retrieve the contents of the specified `url`,
122 | /// `method`, `parameters`, `encoding` and `headers`.
123 | ///
124 | /// - parameter url: The URL.
125 | /// - parameter method: The HTTP method. `.get` by default.
126 | /// - parameter parameters: The parameters. `nil` by default.
127 | /// - parameter encoding: The parameter encoding. `URLEncoding.default` by default.
128 | /// - parameter headers: The HTTP headers. `nil` by default.
129 | ///
130 | /// - returns: The created `DataRequest`.
131 | @discardableResult
132 | public func request(
133 | _ url: URLConvertible,
134 | method: HTTPMethod = .get,
135 | parameters: Parameters? = nil,
136 | encoding: ParameterEncoding = URLEncoding.default,
137 | headers: HTTPHeaders? = nil)
138 | -> DataRequest
139 | {
140 | return SessionManager.default.request(
141 | url,
142 | method: method,
143 | parameters: parameters,
144 | encoding: encoding,
145 | headers: headers
146 | )
147 | }
148 |
149 | /// Creates a `DataRequest` using the default `SessionManager` to retrieve the contents of a URL based on the
150 | /// specified `urlRequest`.
151 | ///
152 | /// - parameter urlRequest: The URL request
153 | ///
154 | /// - returns: The created `DataRequest`.
155 | @discardableResult
156 | public func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
157 | return SessionManager.default.request(urlRequest)
158 | }
159 |
160 | // MARK: - Download Request
161 |
162 | // MARK: URL Request
163 |
164 | /// Creates a `DownloadRequest` using the default `SessionManager` to retrieve the contents of the specified `url`,
165 | /// `method`, `parameters`, `encoding`, `headers` and save them to the `destination`.
166 | ///
167 | /// If `destination` is not specified, the contents will remain in the temporary location determined by the
168 | /// underlying URL session.
169 | ///
170 | /// - parameter url: The URL.
171 | /// - parameter method: The HTTP method. `.get` by default.
172 | /// - parameter parameters: The parameters. `nil` by default.
173 | /// - parameter encoding: The parameter encoding. `URLEncoding.default` by default.
174 | /// - parameter headers: The HTTP headers. `nil` by default.
175 | /// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
176 | ///
177 | /// - returns: The created `DownloadRequest`.
178 | @discardableResult
179 | public func download(
180 | _ url: URLConvertible,
181 | method: HTTPMethod = .get,
182 | parameters: Parameters? = nil,
183 | encoding: ParameterEncoding = URLEncoding.default,
184 | headers: HTTPHeaders? = nil,
185 | to destination: DownloadRequest.DownloadFileDestination? = nil)
186 | -> DownloadRequest
187 | {
188 | return SessionManager.default.download(
189 | url,
190 | method: method,
191 | parameters: parameters,
192 | encoding: encoding,
193 | headers: headers,
194 | to: destination
195 | )
196 | }
197 |
198 | /// Creates a `DownloadRequest` using the default `SessionManager` to retrieve the contents of a URL based on the
199 | /// specified `urlRequest` and save them to the `destination`.
200 | ///
201 | /// If `destination` is not specified, the contents will remain in the temporary location determined by the
202 | /// underlying URL session.
203 | ///
204 | /// - parameter urlRequest: The URL request.
205 | /// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
206 | ///
207 | /// - returns: The created `DownloadRequest`.
208 | @discardableResult
209 | public func download(
210 | _ urlRequest: URLRequestConvertible,
211 | to destination: DownloadRequest.DownloadFileDestination? = nil)
212 | -> DownloadRequest
213 | {
214 | return SessionManager.default.download(urlRequest, to: destination)
215 | }
216 |
217 | // MARK: Resume Data
218 |
219 | /// Creates a `DownloadRequest` using the default `SessionManager` from the `resumeData` produced from a
220 | /// previous request cancellation to retrieve the contents of the original request and save them to the `destination`.
221 | ///
222 | /// If `destination` is not specified, the contents will remain in the temporary location determined by the
223 | /// underlying URL session.
224 | ///
225 | /// On the latest release of all the Apple platforms (iOS 10, macOS 10.12, tvOS 10, watchOS 3), `resumeData` is broken
226 | /// on background URL session configurations. There's an underlying bug in the `resumeData` generation logic where the
227 | /// data is written incorrectly and will always fail to resume the download. For more information about the bug and
228 | /// possible workarounds, please refer to the following Stack Overflow post:
229 | ///
230 | /// - http://stackoverflow.com/a/39347461/1342462
231 | ///
232 | /// - parameter resumeData: The resume data. This is an opaque data blob produced by `URLSessionDownloadTask`
233 | /// when a task is cancelled. See `URLSession -downloadTask(withResumeData:)` for additional
234 | /// information.
235 | /// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
236 | ///
237 | /// - returns: The created `DownloadRequest`.
238 | @discardableResult
239 | public func download(
240 | resumingWith resumeData: Data,
241 | to destination: DownloadRequest.DownloadFileDestination? = nil)
242 | -> DownloadRequest
243 | {
244 | return SessionManager.default.download(resumingWith: resumeData, to: destination)
245 | }
246 |
247 | // MARK: - Upload Request
248 |
249 | // MARK: File
250 |
251 | /// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers`
252 | /// for uploading the `file`.
253 | ///
254 | /// - parameter file: The file to upload.
255 | /// - parameter url: The URL.
256 | /// - parameter method: The HTTP method. `.post` by default.
257 | /// - parameter headers: The HTTP headers. `nil` by default.
258 | ///
259 | /// - returns: The created `UploadRequest`.
260 | @discardableResult
261 | public func upload(
262 | _ fileURL: URL,
263 | to url: URLConvertible,
264 | method: HTTPMethod = .post,
265 | headers: HTTPHeaders? = nil)
266 | -> UploadRequest
267 | {
268 | return SessionManager.default.upload(fileURL, to: url, method: method, headers: headers)
269 | }
270 |
271 | /// Creates a `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for
272 | /// uploading the `file`.
273 | ///
274 | /// - parameter file: The file to upload.
275 | /// - parameter urlRequest: The URL request.
276 | ///
277 | /// - returns: The created `UploadRequest`.
278 | @discardableResult
279 | public func upload(_ fileURL: URL, with urlRequest: URLRequestConvertible) -> UploadRequest {
280 | return SessionManager.default.upload(fileURL, with: urlRequest)
281 | }
282 |
283 | // MARK: Data
284 |
285 | /// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers`
286 | /// for uploading the `data`.
287 | ///
288 | /// - parameter data: The data to upload.
289 | /// - parameter url: The URL.
290 | /// - parameter method: The HTTP method. `.post` by default.
291 | /// - parameter headers: The HTTP headers. `nil` by default.
292 | ///
293 | /// - returns: The created `UploadRequest`.
294 | @discardableResult
295 | public func upload(
296 | _ data: Data,
297 | to url: URLConvertible,
298 | method: HTTPMethod = .post,
299 | headers: HTTPHeaders? = nil)
300 | -> UploadRequest
301 | {
302 | return SessionManager.default.upload(data, to: url, method: method, headers: headers)
303 | }
304 |
305 | /// Creates an `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for
306 | /// uploading the `data`.
307 | ///
308 | /// - parameter data: The data to upload.
309 | /// - parameter urlRequest: The URL request.
310 | ///
311 | /// - returns: The created `UploadRequest`.
312 | @discardableResult
313 | public func upload(_ data: Data, with urlRequest: URLRequestConvertible) -> UploadRequest {
314 | return SessionManager.default.upload(data, with: urlRequest)
315 | }
316 |
317 | // MARK: InputStream
318 |
319 | /// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers`
320 | /// for uploading the `stream`.
321 | ///
322 | /// - parameter stream: The stream to upload.
323 | /// - parameter url: The URL.
324 | /// - parameter method: The HTTP method. `.post` by default.
325 | /// - parameter headers: The HTTP headers. `nil` by default.
326 | ///
327 | /// - returns: The created `UploadRequest`.
328 | @discardableResult
329 | public func upload(
330 | _ stream: InputStream,
331 | to url: URLConvertible,
332 | method: HTTPMethod = .post,
333 | headers: HTTPHeaders? = nil)
334 | -> UploadRequest
335 | {
336 | return SessionManager.default.upload(stream, to: url, method: method, headers: headers)
337 | }
338 |
339 | /// Creates an `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for
340 | /// uploading the `stream`.
341 | ///
342 | /// - parameter urlRequest: The URL request.
343 | /// - parameter stream: The stream to upload.
344 | ///
345 | /// - returns: The created `UploadRequest`.
346 | @discardableResult
347 | public func upload(_ stream: InputStream, with urlRequest: URLRequestConvertible) -> UploadRequest {
348 | return SessionManager.default.upload(stream, with: urlRequest)
349 | }
350 |
351 | // MARK: MultipartFormData
352 |
353 | /// Encodes `multipartFormData` using `encodingMemoryThreshold` with the default `SessionManager` and calls
354 | /// `encodingCompletion` with new `UploadRequest` using the `url`, `method` and `headers`.
355 | ///
356 | /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative
357 | /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most
358 | /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to
359 | /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory
360 | /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be
361 | /// used for larger payloads such as video content.
362 | ///
363 | /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory
364 | /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`,
365 | /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk
366 | /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding
367 | /// technique was used.
368 | ///
369 | /// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`.
370 | /// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes.
371 | /// `multipartFormDataEncodingMemoryThreshold` by default.
372 | /// - parameter url: The URL.
373 | /// - parameter method: The HTTP method. `.post` by default.
374 | /// - parameter headers: The HTTP headers. `nil` by default.
375 | /// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete.
376 | public func upload(
377 | multipartFormData: @escaping (MultipartFormData) -> Void,
378 | usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
379 | to url: URLConvertible,
380 | method: HTTPMethod = .post,
381 | headers: HTTPHeaders? = nil,
382 | encodingCompletion: ((SessionManager.MultipartFormDataEncodingResult) -> Void)?)
383 | {
384 | return SessionManager.default.upload(
385 | multipartFormData: multipartFormData,
386 | usingThreshold: encodingMemoryThreshold,
387 | to: url,
388 | method: method,
389 | headers: headers,
390 | encodingCompletion: encodingCompletion
391 | )
392 | }
393 |
394 | /// Encodes `multipartFormData` using `encodingMemoryThreshold` and the default `SessionManager` and
395 | /// calls `encodingCompletion` with new `UploadRequest` using the `urlRequest`.
396 | ///
397 | /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative
398 | /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most
399 | /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to
400 | /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory
401 | /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be
402 | /// used for larger payloads such as video content.
403 | ///
404 | /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory
405 | /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`,
406 | /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk
407 | /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding
408 | /// technique was used.
409 | ///
410 | /// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`.
411 | /// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes.
412 | /// `multipartFormDataEncodingMemoryThreshold` by default.
413 | /// - parameter urlRequest: The URL request.
414 | /// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete.
415 | public func upload(
416 | multipartFormData: @escaping (MultipartFormData) -> Void,
417 | usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
418 | with urlRequest: URLRequestConvertible,
419 | encodingCompletion: ((SessionManager.MultipartFormDataEncodingResult) -> Void)?)
420 | {
421 | return SessionManager.default.upload(
422 | multipartFormData: multipartFormData,
423 | usingThreshold: encodingMemoryThreshold,
424 | with: urlRequest,
425 | encodingCompletion: encodingCompletion
426 | )
427 | }
428 |
429 | #if !os(watchOS)
430 |
431 | // MARK: - Stream Request
432 |
433 | // MARK: Hostname and Port
434 |
435 | /// Creates a `StreamRequest` using the default `SessionManager` for bidirectional streaming with the `hostname`
436 | /// and `port`.
437 | ///
438 | /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
439 | ///
440 | /// - parameter hostName: The hostname of the server to connect to.
441 | /// - parameter port: The port of the server to connect to.
442 | ///
443 | /// - returns: The created `StreamRequest`.
444 | @discardableResult
445 | @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
446 | public func stream(withHostName hostName: String, port: Int) -> StreamRequest {
447 | return SessionManager.default.stream(withHostName: hostName, port: port)
448 | }
449 |
450 | // MARK: NetService
451 |
452 | /// Creates a `StreamRequest` using the default `SessionManager` for bidirectional streaming with the `netService`.
453 | ///
454 | /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
455 | ///
456 | /// - parameter netService: The net service used to identify the endpoint.
457 | ///
458 | /// - returns: The created `StreamRequest`.
459 | @discardableResult
460 | @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
461 | public func stream(with netService: NetService) -> StreamRequest {
462 | return SessionManager.default.stream(with: netService)
463 | }
464 |
465 | #endif
466 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/DispatchQueue+Alamofire.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DispatchQueue+Alamofire.swift
3 | //
4 | // Copyright (c) 2014-2016 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 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/NetworkReachabilityManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkReachabilityManager.swift
3 | //
4 | // Copyright (c) 2014-2016 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 | #if !os(watchOS)
26 |
27 | import Foundation
28 | import SystemConfiguration
29 |
30 | /// The `NetworkReachabilityManager` class listens for reachability changes of hosts and addresses for both WWAN and
31 | /// WiFi network interfaces.
32 | ///
33 | /// Reachability can be used to determine background information about why a network operation failed, or to retry
34 | /// network requests when a connection is established. It should not be used to prevent a user from initiating a network
35 | /// request, as it's possible that an initial request may be required to establish reachability.
36 | public class NetworkReachabilityManager {
37 | /// Defines the various states of network reachability.
38 | ///
39 | /// - unknown: It is unknown whether the network is reachable.
40 | /// - notReachable: The network is not reachable.
41 | /// - reachable: The network is reachable.
42 | public enum NetworkReachabilityStatus {
43 | case unknown
44 | case notReachable
45 | case reachable(ConnectionType)
46 | }
47 |
48 | /// Defines the various connection types detected by reachability flags.
49 | ///
50 | /// - ethernetOrWiFi: The connection type is either over Ethernet or WiFi.
51 | /// - wwan: The connection type is a WWAN connection.
52 | public enum ConnectionType {
53 | case ethernetOrWiFi
54 | case wwan
55 | }
56 |
57 | /// A closure executed when the network reachability status changes. The closure takes a single argument: the
58 | /// network reachability status.
59 | public typealias Listener = (NetworkReachabilityStatus) -> Void
60 |
61 | // MARK: - Properties
62 |
63 | /// Whether the network is currently reachable.
64 | public var isReachable: Bool { return isReachableOnWWAN || isReachableOnEthernetOrWiFi }
65 |
66 | /// Whether the network is currently reachable over the WWAN interface.
67 | public var isReachableOnWWAN: Bool { return networkReachabilityStatus == .reachable(.wwan) }
68 |
69 | /// Whether the network is currently reachable over Ethernet or WiFi interface.
70 | public var isReachableOnEthernetOrWiFi: Bool { return networkReachabilityStatus == .reachable(.ethernetOrWiFi) }
71 |
72 | /// The current network reachability status.
73 | public var networkReachabilityStatus: NetworkReachabilityStatus {
74 | guard let flags = self.flags else { return .unknown }
75 | return networkReachabilityStatusForFlags(flags)
76 | }
77 |
78 | /// The dispatch queue to execute the `listener` closure on.
79 | public var listenerQueue: DispatchQueue = DispatchQueue.main
80 |
81 | /// A closure executed when the network reachability status changes.
82 | public var listener: Listener?
83 |
84 | private var flags: SCNetworkReachabilityFlags? {
85 | var flags = SCNetworkReachabilityFlags()
86 |
87 | if SCNetworkReachabilityGetFlags(reachability, &flags) {
88 | return flags
89 | }
90 |
91 | return nil
92 | }
93 |
94 | private let reachability: SCNetworkReachability
95 | private var previousFlags: SCNetworkReachabilityFlags
96 |
97 | // MARK: - Initialization
98 |
99 | /// Creates a `NetworkReachabilityManager` instance with the specified host.
100 | ///
101 | /// - parameter host: The host used to evaluate network reachability.
102 | ///
103 | /// - returns: The new `NetworkReachabilityManager` instance.
104 | public convenience init?(host: String) {
105 | guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
106 | self.init(reachability: reachability)
107 | }
108 |
109 | /// Creates a `NetworkReachabilityManager` instance that monitors the address 0.0.0.0.
110 | ///
111 | /// Reachability treats the 0.0.0.0 address as a special token that causes it to monitor the general routing
112 | /// status of the device, both IPv4 and IPv6.
113 | ///
114 | /// - returns: The new `NetworkReachabilityManager` instance.
115 | public convenience init?() {
116 | var address = sockaddr_in()
117 | address.sin_len = UInt8(MemoryLayout.size)
118 | address.sin_family = sa_family_t(AF_INET)
119 |
120 | guard let reachability = withUnsafePointer(to: &address, { pointer in
121 | return pointer.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size) {
122 | return SCNetworkReachabilityCreateWithAddress(nil, $0)
123 | }
124 | }) else { return nil }
125 |
126 | self.init(reachability: reachability)
127 | }
128 |
129 | private init(reachability: SCNetworkReachability) {
130 | self.reachability = reachability
131 | self.previousFlags = SCNetworkReachabilityFlags()
132 | }
133 |
134 | deinit {
135 | stopListening()
136 | }
137 |
138 | // MARK: - Listening
139 |
140 | /// Starts listening for changes in network reachability status.
141 | ///
142 | /// - returns: `true` if listening was started successfully, `false` otherwise.
143 | @discardableResult
144 | public func startListening() -> Bool {
145 | var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
146 | context.info = Unmanaged.passUnretained(self).toOpaque()
147 |
148 | let callbackEnabled = SCNetworkReachabilitySetCallback(
149 | reachability,
150 | { (_, flags, info) in
151 | let reachability = Unmanaged.fromOpaque(info!).takeUnretainedValue()
152 | reachability.notifyListener(flags)
153 | },
154 | &context
155 | )
156 |
157 | let queueEnabled = SCNetworkReachabilitySetDispatchQueue(reachability, listenerQueue)
158 |
159 | listenerQueue.async {
160 | self.previousFlags = SCNetworkReachabilityFlags()
161 | self.notifyListener(self.flags ?? SCNetworkReachabilityFlags())
162 | }
163 |
164 | return callbackEnabled && queueEnabled
165 | }
166 |
167 | /// Stops listening for changes in network reachability status.
168 | public func stopListening() {
169 | SCNetworkReachabilitySetCallback(reachability, nil, nil)
170 | SCNetworkReachabilitySetDispatchQueue(reachability, nil)
171 | }
172 |
173 | // MARK: - Internal - Listener Notification
174 |
175 | func notifyListener(_ flags: SCNetworkReachabilityFlags) {
176 | guard previousFlags != flags else { return }
177 | previousFlags = flags
178 |
179 | listener?(networkReachabilityStatusForFlags(flags))
180 | }
181 |
182 | // MARK: - Internal - Network Reachability Status
183 |
184 | func networkReachabilityStatusForFlags(_ flags: SCNetworkReachabilityFlags) -> NetworkReachabilityStatus {
185 | guard flags.contains(.reachable) else { return .notReachable }
186 |
187 | var networkStatus: NetworkReachabilityStatus = .notReachable
188 |
189 | if !flags.contains(.connectionRequired) { networkStatus = .reachable(.ethernetOrWiFi) }
190 |
191 | if flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic) {
192 | if !flags.contains(.interventionRequired) { networkStatus = .reachable(.ethernetOrWiFi) }
193 | }
194 |
195 | #if os(iOS)
196 | if flags.contains(.isWWAN) { networkStatus = .reachable(.wwan) }
197 | #endif
198 |
199 | return networkStatus
200 | }
201 | }
202 |
203 | // MARK: -
204 |
205 | extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable {}
206 |
207 | /// Returns whether the two network reachability status values are equal.
208 | ///
209 | /// - parameter lhs: The left-hand side value to compare.
210 | /// - parameter rhs: The right-hand side value to compare.
211 | ///
212 | /// - returns: `true` if the two values are equal, `false` otherwise.
213 | public func ==(
214 | lhs: NetworkReachabilityManager.NetworkReachabilityStatus,
215 | rhs: NetworkReachabilityManager.NetworkReachabilityStatus)
216 | -> Bool
217 | {
218 | switch (lhs, rhs) {
219 | case (.unknown, .unknown):
220 | return true
221 | case (.notReachable, .notReachable):
222 | return true
223 | case let (.reachable(lhsConnectionType), .reachable(rhsConnectionType)):
224 | return lhsConnectionType == rhsConnectionType
225 | default:
226 | return false
227 | }
228 | }
229 |
230 | #endif
231 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/Notifications.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Notifications.swift
3 | //
4 | // Copyright (c) 2014-2016 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 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/ParameterEncoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParameterEncoding.swift
3 | //
4 | // Copyright (c) 2014-2016 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 | /// HTTP method definitions.
28 | ///
29 | /// See https://tools.ietf.org/html/rfc7231#section-4.3
30 | public enum HTTPMethod: String {
31 | case options = "OPTIONS"
32 | case get = "GET"
33 | case head = "HEAD"
34 | case post = "POST"
35 | case put = "PUT"
36 | case patch = "PATCH"
37 | case delete = "DELETE"
38 | case trace = "TRACE"
39 | case connect = "CONNECT"
40 | }
41 |
42 | // MARK: -
43 |
44 | /// A dictionary of parameters to apply to a `URLRequest`.
45 | public typealias Parameters = [String: Any]
46 |
47 | /// A type used to define how a set of parameters are applied to a `URLRequest`.
48 | public protocol ParameterEncoding {
49 | /// Creates a URL request by encoding parameters and applying them onto an existing request.
50 | ///
51 | /// - parameter urlRequest: The request to have parameters applied.
52 | /// - parameter parameters: The parameters to apply.
53 | ///
54 | /// - throws: An `AFError.parameterEncodingFailed` error if encoding fails.
55 | ///
56 | /// - returns: The encoded request.
57 | func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
58 | }
59 |
60 | // MARK: -
61 |
62 | /// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP
63 | /// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as
64 | /// the HTTP body depends on the destination of the encoding.
65 | ///
66 | /// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to
67 | /// `application/x-www-form-urlencoded; charset=utf-8`. Since there is no published specification for how to encode
68 | /// collection types, the convention of appending `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending
69 | /// the key surrounded by square brackets for nested dictionary values (`foo[bar]=baz`).
70 | public struct URLEncoding: ParameterEncoding {
71 |
72 | // MARK: Helper Types
73 |
74 | /// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the
75 | /// resulting URL request.
76 | ///
77 | /// - methodDependent: Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE`
78 | /// requests and sets as the HTTP body for requests with any other HTTP method.
79 | /// - queryString: Sets or appends encoded query string result to existing query string.
80 | /// - httpBody: Sets encoded query string result as the HTTP body of the URL request.
81 | public enum Destination {
82 | case methodDependent, queryString, httpBody
83 | }
84 |
85 | // MARK: Properties
86 |
87 | /// Returns a default `URLEncoding` instance.
88 | public static var `default`: URLEncoding { return URLEncoding() }
89 |
90 | /// Returns a `URLEncoding` instance with a `.methodDependent` destination.
91 | public static var methodDependent: URLEncoding { return URLEncoding() }
92 |
93 | /// Returns a `URLEncoding` instance with a `.queryString` destination.
94 | public static var queryString: URLEncoding { return URLEncoding(destination: .queryString) }
95 |
96 | /// Returns a `URLEncoding` instance with an `.httpBody` destination.
97 | public static var httpBody: URLEncoding { return URLEncoding(destination: .httpBody) }
98 |
99 | /// The destination defining where the encoded query string is to be applied to the URL request.
100 | public let destination: Destination
101 |
102 | // MARK: Initialization
103 |
104 | /// Creates a `URLEncoding` instance using the specified destination.
105 | ///
106 | /// - parameter destination: The destination defining where the encoded query string is to be applied.
107 | ///
108 | /// - returns: The new `URLEncoding` instance.
109 | public init(destination: Destination = .methodDependent) {
110 | self.destination = destination
111 | }
112 |
113 | // MARK: Encoding
114 |
115 | /// Creates a URL request by encoding parameters and applying them onto an existing request.
116 | ///
117 | /// - parameter urlRequest: The request to have parameters applied.
118 | /// - parameter parameters: The parameters to apply.
119 | ///
120 | /// - throws: An `Error` if the encoding process encounters an error.
121 | ///
122 | /// - returns: The encoded request.
123 | public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
124 | var urlRequest = try urlRequest.asURLRequest()
125 |
126 | guard let parameters = parameters else { return urlRequest }
127 |
128 | if let method = HTTPMethod(rawValue: urlRequest.httpMethod ?? "GET"), encodesParametersInURL(with: method) {
129 | guard let url = urlRequest.url else {
130 | throw AFError.parameterEncodingFailed(reason: .missingURL)
131 | }
132 |
133 | if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
134 | let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
135 | urlComponents.percentEncodedQuery = percentEncodedQuery
136 | urlRequest.url = urlComponents.url
137 | }
138 | } else {
139 | if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
140 | urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
141 | }
142 |
143 | urlRequest.httpBody = query(parameters).data(using: .utf8, allowLossyConversion: false)
144 | }
145 |
146 | return urlRequest
147 | }
148 |
149 | /// Creates percent-escaped, URL encoded query string components from the given key-value pair using recursion.
150 | ///
151 | /// - parameter key: The key of the query component.
152 | /// - parameter value: The value of the query component.
153 | ///
154 | /// - returns: The percent-escaped, URL encoded query string components.
155 | public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
156 | var components: [(String, String)] = []
157 |
158 | if let dictionary = value as? [String: Any] {
159 | for (nestedKey, value) in dictionary {
160 | components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
161 | }
162 | } else if let array = value as? [Any] {
163 | for value in array {
164 | components += queryComponents(fromKey: "\(key)[]", value: value)
165 | }
166 | } else if let value = value as? NSNumber {
167 | if value.isBool {
168 | components.append((escape(key), escape((value.boolValue ? "1" : "0"))))
169 | } else {
170 | components.append((escape(key), escape("\(value)")))
171 | }
172 | } else if let bool = value as? Bool {
173 | components.append((escape(key), escape((bool ? "1" : "0"))))
174 | } else {
175 | components.append((escape(key), escape("\(value)")))
176 | }
177 |
178 | return components
179 | }
180 |
181 | /// Returns a percent-escaped string following RFC 3986 for a query string key or value.
182 | ///
183 | /// RFC 3986 states that the following characters are "reserved" characters.
184 | ///
185 | /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/"
186 | /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
187 | ///
188 | /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
189 | /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
190 | /// should be percent-escaped in the query string.
191 | ///
192 | /// - parameter string: The string to be percent-escaped.
193 | ///
194 | /// - returns: The percent-escaped string.
195 | public func escape(_ string: String) -> String {
196 | let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
197 | let subDelimitersToEncode = "!$&'()*+,;="
198 |
199 | var allowedCharacterSet = CharacterSet.urlQueryAllowed
200 | allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
201 |
202 | var escaped = ""
203 |
204 | //==========================================================================================================
205 | //
206 | // Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
207 | // hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
208 | // longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
209 | // info, please refer to:
210 | //
211 | // - https://github.com/Alamofire/Alamofire/issues/206
212 | //
213 | //==========================================================================================================
214 |
215 | if #available(iOS 8.3, *) {
216 | escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
217 | } else {
218 | let batchSize = 50
219 | var index = string.startIndex
220 |
221 | while index != string.endIndex {
222 | let startIndex = index
223 | let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex
224 | let range = startIndex.. String {
238 | var components: [(String, String)] = []
239 |
240 | for key in parameters.keys.sorted(by: <) {
241 | let value = parameters[key]!
242 | components += queryComponents(fromKey: key, value: value)
243 | }
244 |
245 | return components.map { "\($0)=\($1)" }.joined(separator: "&")
246 | }
247 |
248 | private func encodesParametersInURL(with method: HTTPMethod) -> Bool {
249 | switch destination {
250 | case .queryString:
251 | return true
252 | case .httpBody:
253 | return false
254 | default:
255 | break
256 | }
257 |
258 | switch method {
259 | case .get, .head, .delete:
260 | return true
261 | default:
262 | return false
263 | }
264 | }
265 | }
266 |
267 | // MARK: -
268 |
269 | /// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the
270 | /// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`.
271 | public struct JSONEncoding: ParameterEncoding {
272 |
273 | // MARK: Properties
274 |
275 | /// Returns a `JSONEncoding` instance with default writing options.
276 | public static var `default`: JSONEncoding { return JSONEncoding() }
277 |
278 | /// Returns a `JSONEncoding` instance with `.prettyPrinted` writing options.
279 | public static var prettyPrinted: JSONEncoding { return JSONEncoding(options: .prettyPrinted) }
280 |
281 | /// The options for writing the parameters as JSON data.
282 | public let options: JSONSerialization.WritingOptions
283 |
284 | // MARK: Initialization
285 |
286 | /// Creates a `JSONEncoding` instance using the specified options.
287 | ///
288 | /// - parameter options: The options for writing the parameters as JSON data.
289 | ///
290 | /// - returns: The new `JSONEncoding` instance.
291 | public init(options: JSONSerialization.WritingOptions = []) {
292 | self.options = options
293 | }
294 |
295 | // MARK: Encoding
296 |
297 | /// Creates a URL request by encoding parameters and applying them onto an existing request.
298 | ///
299 | /// - parameter urlRequest: The request to have parameters applied.
300 | /// - parameter parameters: The parameters to apply.
301 | ///
302 | /// - throws: An `Error` if the encoding process encounters an error.
303 | ///
304 | /// - returns: The encoded request.
305 | public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
306 | var urlRequest = try urlRequest.asURLRequest()
307 |
308 | guard let parameters = parameters else { return urlRequest }
309 |
310 | do {
311 | let data = try JSONSerialization.data(withJSONObject: parameters, options: options)
312 |
313 | if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
314 | urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
315 | }
316 |
317 | urlRequest.httpBody = data
318 | } catch {
319 | throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
320 | }
321 |
322 | return urlRequest
323 | }
324 |
325 | /// Creates a URL request by encoding the JSON object and setting the resulting data on the HTTP body.
326 | ///
327 | /// - parameter urlRequest: The request to apply the JSON object to.
328 | /// - parameter jsonObject: The JSON object to apply to the request.
329 | ///
330 | /// - throws: An `Error` if the encoding process encounters an error.
331 | ///
332 | /// - returns: The encoded request.
333 | public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest {
334 | var urlRequest = try urlRequest.asURLRequest()
335 |
336 | guard let jsonObject = jsonObject else { return urlRequest }
337 |
338 | do {
339 | let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options)
340 |
341 | if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
342 | urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
343 | }
344 |
345 | urlRequest.httpBody = data
346 | } catch {
347 | throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
348 | }
349 |
350 | return urlRequest
351 | }
352 | }
353 |
354 | // MARK: -
355 |
356 | /// Uses `PropertyListSerialization` to create a plist representation of the parameters object, according to the
357 | /// associated format and write options values, which is set as the body of the request. The `Content-Type` HTTP header
358 | /// field of an encoded request is set to `application/x-plist`.
359 | public struct PropertyListEncoding: ParameterEncoding {
360 |
361 | // MARK: Properties
362 |
363 | /// Returns a default `PropertyListEncoding` instance.
364 | public static var `default`: PropertyListEncoding { return PropertyListEncoding() }
365 |
366 | /// Returns a `PropertyListEncoding` instance with xml formatting and default writing options.
367 | public static var xml: PropertyListEncoding { return PropertyListEncoding(format: .xml) }
368 |
369 | /// Returns a `PropertyListEncoding` instance with binary formatting and default writing options.
370 | public static var binary: PropertyListEncoding { return PropertyListEncoding(format: .binary) }
371 |
372 | /// The property list serialization format.
373 | public let format: PropertyListSerialization.PropertyListFormat
374 |
375 | /// The options for writing the parameters as plist data.
376 | public let options: PropertyListSerialization.WriteOptions
377 |
378 | // MARK: Initialization
379 |
380 | /// Creates a `PropertyListEncoding` instance using the specified format and options.
381 | ///
382 | /// - parameter format: The property list serialization format.
383 | /// - parameter options: The options for writing the parameters as plist data.
384 | ///
385 | /// - returns: The new `PropertyListEncoding` instance.
386 | public init(
387 | format: PropertyListSerialization.PropertyListFormat = .xml,
388 | options: PropertyListSerialization.WriteOptions = 0)
389 | {
390 | self.format = format
391 | self.options = options
392 | }
393 |
394 | // MARK: Encoding
395 |
396 | /// Creates a URL request by encoding parameters and applying them onto an existing request.
397 | ///
398 | /// - parameter urlRequest: The request to have parameters applied.
399 | /// - parameter parameters: The parameters to apply.
400 | ///
401 | /// - throws: An `Error` if the encoding process encounters an error.
402 | ///
403 | /// - returns: The encoded request.
404 | public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
405 | var urlRequest = try urlRequest.asURLRequest()
406 |
407 | guard let parameters = parameters else { return urlRequest }
408 |
409 | do {
410 | let data = try PropertyListSerialization.data(
411 | fromPropertyList: parameters,
412 | format: format,
413 | options: options
414 | )
415 |
416 | if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
417 | urlRequest.setValue("application/x-plist", forHTTPHeaderField: "Content-Type")
418 | }
419 |
420 | urlRequest.httpBody = data
421 | } catch {
422 | throw AFError.parameterEncodingFailed(reason: .propertyListEncodingFailed(error: error))
423 | }
424 |
425 | return urlRequest
426 | }
427 | }
428 |
429 | // MARK: -
430 |
431 | extension NSNumber {
432 | fileprivate var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) }
433 | }
434 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/Response.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Response.swift
3 | //
4 | // Copyright (c) 2014-2016 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 | /// Used to store all data associated with an non-serialized response of a data or upload request.
28 | public struct DefaultDataResponse {
29 | /// The URL request sent to the server.
30 | public let request: URLRequest?
31 |
32 | /// The server's response to the URL request.
33 | public let response: HTTPURLResponse?
34 |
35 | /// The data returned by the server.
36 | public let data: Data?
37 |
38 | /// The error encountered while executing or validating the request.
39 | public let error: Error?
40 |
41 | /// The timeline of the complete lifecycle of the request.
42 | public let timeline: Timeline
43 |
44 | var _metrics: AnyObject?
45 |
46 | /// Creates a `DefaultDataResponse` instance from the specified parameters.
47 | ///
48 | /// - Parameters:
49 | /// - request: The URL request sent to the server.
50 | /// - response: The server's response to the URL request.
51 | /// - data: The data returned by the server.
52 | /// - error: The error encountered while executing or validating the request.
53 | /// - timeline: The timeline of the complete lifecycle of the request. `Timeline()` by default.
54 | /// - metrics: The task metrics containing the request / response statistics. `nil` by default.
55 | public init(
56 | request: URLRequest?,
57 | response: HTTPURLResponse?,
58 | data: Data?,
59 | error: Error?,
60 | timeline: Timeline = Timeline(),
61 | metrics: AnyObject? = nil)
62 | {
63 | self.request = request
64 | self.response = response
65 | self.data = data
66 | self.error = error
67 | self.timeline = timeline
68 | }
69 | }
70 |
71 | // MARK: -
72 |
73 | /// Used to store all data associated with a serialized response of a data or upload request.
74 | public struct DataResponse {
75 | /// The URL request sent to the server.
76 | public let request: URLRequest?
77 |
78 | /// The server's response to the URL request.
79 | public let response: HTTPURLResponse?
80 |
81 | /// The data returned by the server.
82 | public let data: Data?
83 |
84 | /// The result of response serialization.
85 | public let result: Result
86 |
87 | /// The timeline of the complete lifecycle of the request.
88 | public let timeline: Timeline
89 |
90 | /// Returns the associated value of the result if it is a success, `nil` otherwise.
91 | public var value: Value? { return result.value }
92 |
93 | /// Returns the associated error value if the result if it is a failure, `nil` otherwise.
94 | public var error: Error? { return result.error }
95 |
96 | var _metrics: AnyObject?
97 |
98 | /// Creates a `DataResponse` instance with the specified parameters derived from response serialization.
99 | ///
100 | /// - parameter request: The URL request sent to the server.
101 | /// - parameter response: The server's response to the URL request.
102 | /// - parameter data: The data returned by the server.
103 | /// - parameter result: The result of response serialization.
104 | /// - parameter timeline: The timeline of the complete lifecycle of the `Request`. Defaults to `Timeline()`.
105 | ///
106 | /// - returns: The new `DataResponse` instance.
107 | public init(
108 | request: URLRequest?,
109 | response: HTTPURLResponse?,
110 | data: Data?,
111 | result: Result,
112 | timeline: Timeline = Timeline())
113 | {
114 | self.request = request
115 | self.response = response
116 | self.data = data
117 | self.result = result
118 | self.timeline = timeline
119 | }
120 | }
121 |
122 | // MARK: -
123 |
124 | extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible {
125 | /// The textual representation used when written to an output stream, which includes whether the result was a
126 | /// success or failure.
127 | public var description: String {
128 | return result.debugDescription
129 | }
130 |
131 | /// The debug textual representation used when written to an output stream, which includes the URL request, the URL
132 | /// response, the server data, the response serialization result and the timeline.
133 | public var debugDescription: String {
134 | var output: [String] = []
135 |
136 | output.append(request != nil ? "[Request]: \(request!.httpMethod ?? "GET") \(request!)" : "[Request]: nil")
137 | output.append(response != nil ? "[Response]: \(response!)" : "[Response]: nil")
138 | output.append("[Data]: \(data?.count ?? 0) bytes")
139 | output.append("[Result]: \(result.debugDescription)")
140 | output.append("[Timeline]: \(timeline.debugDescription)")
141 |
142 | return output.joined(separator: "\n")
143 | }
144 | }
145 |
146 | // MARK: -
147 |
148 | /// Used to store all data associated with an non-serialized response of a download request.
149 | public struct DefaultDownloadResponse {
150 | /// The URL request sent to the server.
151 | public let request: URLRequest?
152 |
153 | /// The server's response to the URL request.
154 | public let response: HTTPURLResponse?
155 |
156 | /// The temporary destination URL of the data returned from the server.
157 | public let temporaryURL: URL?
158 |
159 | /// The final destination URL of the data returned from the server if it was moved.
160 | public let destinationURL: URL?
161 |
162 | /// The resume data generated if the request was cancelled.
163 | public let resumeData: Data?
164 |
165 | /// The error encountered while executing or validating the request.
166 | public let error: Error?
167 |
168 | /// The timeline of the complete lifecycle of the request.
169 | public let timeline: Timeline
170 |
171 | var _metrics: AnyObject?
172 |
173 | /// Creates a `DefaultDownloadResponse` instance from the specified parameters.
174 | ///
175 | /// - Parameters:
176 | /// - request: The URL request sent to the server.
177 | /// - response: The server's response to the URL request.
178 | /// - temporaryURL: The temporary destination URL of the data returned from the server.
179 | /// - destinationURL: The final destination URL of the data returned from the server if it was moved.
180 | /// - resumeData: The resume data generated if the request was cancelled.
181 | /// - error: The error encountered while executing or validating the request.
182 | /// - timeline: The timeline of the complete lifecycle of the request. `Timeline()` by default.
183 | /// - metrics: The task metrics containing the request / response statistics. `nil` by default.
184 | public init(
185 | request: URLRequest?,
186 | response: HTTPURLResponse?,
187 | temporaryURL: URL?,
188 | destinationURL: URL?,
189 | resumeData: Data?,
190 | error: Error?,
191 | timeline: Timeline = Timeline(),
192 | metrics: AnyObject? = nil)
193 | {
194 | self.request = request
195 | self.response = response
196 | self.temporaryURL = temporaryURL
197 | self.destinationURL = destinationURL
198 | self.resumeData = resumeData
199 | self.error = error
200 | self.timeline = timeline
201 | }
202 | }
203 |
204 | // MARK: -
205 |
206 | /// Used to store all data associated with a serialized response of a download request.
207 | public struct DownloadResponse {
208 | /// The URL request sent to the server.
209 | public let request: URLRequest?
210 |
211 | /// The server's response to the URL request.
212 | public let response: HTTPURLResponse?
213 |
214 | /// The temporary destination URL of the data returned from the server.
215 | public let temporaryURL: URL?
216 |
217 | /// The final destination URL of the data returned from the server if it was moved.
218 | public let destinationURL: URL?
219 |
220 | /// The resume data generated if the request was cancelled.
221 | public let resumeData: Data?
222 |
223 | /// The result of response serialization.
224 | public let result: Result
225 |
226 | /// The timeline of the complete lifecycle of the request.
227 | public let timeline: Timeline
228 |
229 | /// Returns the associated value of the result if it is a success, `nil` otherwise.
230 | public var value: Value? { return result.value }
231 |
232 | /// Returns the associated error value if the result if it is a failure, `nil` otherwise.
233 | public var error: Error? { return result.error }
234 |
235 | var _metrics: AnyObject?
236 |
237 | /// Creates a `DownloadResponse` instance with the specified parameters derived from response serialization.
238 | ///
239 | /// - parameter request: The URL request sent to the server.
240 | /// - parameter response: The server's response to the URL request.
241 | /// - parameter temporaryURL: The temporary destination URL of the data returned from the server.
242 | /// - parameter destinationURL: The final destination URL of the data returned from the server if it was moved.
243 | /// - parameter resumeData: The resume data generated if the request was cancelled.
244 | /// - parameter result: The result of response serialization.
245 | /// - parameter timeline: The timeline of the complete lifecycle of the `Request`. Defaults to `Timeline()`.
246 | ///
247 | /// - returns: The new `DownloadResponse` instance.
248 | public init(
249 | request: URLRequest?,
250 | response: HTTPURLResponse?,
251 | temporaryURL: URL?,
252 | destinationURL: URL?,
253 | resumeData: Data?,
254 | result: Result,
255 | timeline: Timeline = Timeline())
256 | {
257 | self.request = request
258 | self.response = response
259 | self.temporaryURL = temporaryURL
260 | self.destinationURL = destinationURL
261 | self.resumeData = resumeData
262 | self.result = result
263 | self.timeline = timeline
264 | }
265 | }
266 |
267 | // MARK: -
268 |
269 | extension DownloadResponse: CustomStringConvertible, CustomDebugStringConvertible {
270 | /// The textual representation used when written to an output stream, which includes whether the result was a
271 | /// success or failure.
272 | public var description: String {
273 | return result.debugDescription
274 | }
275 |
276 | /// The debug textual representation used when written to an output stream, which includes the URL request, the URL
277 | /// response, the temporary and destination URLs, the resume data, the response serialization result and the
278 | /// timeline.
279 | public var debugDescription: String {
280 | var output: [String] = []
281 |
282 | output.append(request != nil ? "[Request]: \(request!.httpMethod ?? "GET") \(request!)" : "[Request]: nil")
283 | output.append(response != nil ? "[Response]: \(response!)" : "[Response]: nil")
284 | output.append("[TemporaryURL]: \(temporaryURL?.path ?? "nil")")
285 | output.append("[DestinationURL]: \(destinationURL?.path ?? "nil")")
286 | output.append("[ResumeData]: \(resumeData?.count ?? 0) bytes")
287 | output.append("[Result]: \(result.debugDescription)")
288 | output.append("[Timeline]: \(timeline.debugDescription)")
289 |
290 | return output.joined(separator: "\n")
291 | }
292 | }
293 |
294 | // MARK: -
295 |
296 | protocol Response {
297 | /// The task metrics containing the request / response statistics.
298 | var _metrics: AnyObject? { get set }
299 | mutating func add(_ metrics: AnyObject?)
300 | }
301 |
302 | extension Response {
303 | mutating func add(_ metrics: AnyObject?) {
304 | #if !os(watchOS)
305 | guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) else { return }
306 | guard let metrics = metrics as? URLSessionTaskMetrics else { return }
307 |
308 | _metrics = metrics
309 | #endif
310 | }
311 | }
312 |
313 | // MARK: -
314 |
315 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
316 | extension DefaultDataResponse: Response {
317 | #if !os(watchOS)
318 | /// The task metrics containing the request / response statistics.
319 | public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
320 | #endif
321 | }
322 |
323 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
324 | extension DataResponse: Response {
325 | #if !os(watchOS)
326 | /// The task metrics containing the request / response statistics.
327 | public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
328 | #endif
329 | }
330 |
331 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
332 | extension DefaultDownloadResponse: Response {
333 | #if !os(watchOS)
334 | /// The task metrics containing the request / response statistics.
335 | public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
336 | #endif
337 | }
338 |
339 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
340 | extension DownloadResponse: Response {
341 | #if !os(watchOS)
342 | /// The task metrics containing the request / response statistics.
343 | public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
344 | #endif
345 | }
346 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/Result.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Result.swift
3 | //
4 | // Copyright (c) 2014-2016 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 | /// Used to represent whether a request was successful or encountered an error.
28 | ///
29 | /// - success: The request and all post processing operations were successful resulting in the serialization of the
30 | /// provided associated value.
31 | ///
32 | /// - failure: The request encountered an error resulting in a failure. The associated values are the original data
33 | /// provided by the server as well as the error that caused the failure.
34 | public enum Result {
35 | case success(Value)
36 | case failure(Error)
37 |
38 | /// Returns `true` if the result is a success, `false` otherwise.
39 | public var isSuccess: Bool {
40 | switch self {
41 | case .success:
42 | return true
43 | case .failure:
44 | return false
45 | }
46 | }
47 |
48 | /// Returns `true` if the result is a failure, `false` otherwise.
49 | public var isFailure: Bool {
50 | return !isSuccess
51 | }
52 |
53 | /// Returns the associated value if the result is a success, `nil` otherwise.
54 | public var value: Value? {
55 | switch self {
56 | case .success(let value):
57 | return value
58 | case .failure:
59 | return nil
60 | }
61 | }
62 |
63 | /// Returns the associated error value if the result is a failure, `nil` otherwise.
64 | public var error: Error? {
65 | switch self {
66 | case .success:
67 | return nil
68 | case .failure(let error):
69 | return error
70 | }
71 | }
72 | }
73 |
74 | // MARK: - CustomStringConvertible
75 |
76 | extension Result: CustomStringConvertible {
77 | /// The textual representation used when written to an output stream, which includes whether the result was a
78 | /// success or failure.
79 | public var description: String {
80 | switch self {
81 | case .success:
82 | return "SUCCESS"
83 | case .failure:
84 | return "FAILURE"
85 | }
86 | }
87 | }
88 |
89 | // MARK: - CustomDebugStringConvertible
90 |
91 | extension Result: CustomDebugStringConvertible {
92 | /// The debug textual representation used when written to an output stream, which includes whether the result was a
93 | /// success or failure in addition to the value or error.
94 | public var debugDescription: String {
95 | switch self {
96 | case .success(let value):
97 | return "SUCCESS: \(value)"
98 | case .failure(let error):
99 | return "FAILURE: \(error)"
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/ServerTrustPolicy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServerTrustPolicy.swift
3 | //
4 | // Copyright (c) 2014-2016 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 managing the mapping of `ServerTrustPolicy` objects to a given host.
28 | open class ServerTrustPolicyManager {
29 | /// The dictionary of policies mapped to a particular host.
30 | open let policies: [String: ServerTrustPolicy]
31 |
32 | /// Initializes the `ServerTrustPolicyManager` instance with the given policies.
33 | ///
34 | /// Since different servers and web services can have different leaf certificates, intermediate and even root
35 | /// certficates, it is important to have the flexibility to specify evaluation policies on a per host basis. This
36 | /// allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key
37 | /// pinning for host3 and disabling evaluation for host4.
38 | ///
39 | /// - parameter policies: A dictionary of all policies mapped to a particular host.
40 | ///
41 | /// - returns: The new `ServerTrustPolicyManager` instance.
42 | public init(policies: [String: ServerTrustPolicy]) {
43 | self.policies = policies
44 | }
45 |
46 | /// Returns the `ServerTrustPolicy` for the given host if applicable.
47 | ///
48 | /// By default, this method will return the policy that perfectly matches the given host. Subclasses could override
49 | /// this method and implement more complex mapping implementations such as wildcards.
50 | ///
51 | /// - parameter host: The host to use when searching for a matching policy.
52 | ///
53 | /// - returns: The server trust policy for the given host if found.
54 | open func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
55 | return policies[host]
56 | }
57 | }
58 |
59 | // MARK: -
60 |
61 | extension URLSession {
62 | private struct AssociatedKeys {
63 | static var managerKey = "URLSession.ServerTrustPolicyManager"
64 | }
65 |
66 | var serverTrustPolicyManager: ServerTrustPolicyManager? {
67 | get {
68 | return objc_getAssociatedObject(self, &AssociatedKeys.managerKey) as? ServerTrustPolicyManager
69 | }
70 | set (manager) {
71 | objc_setAssociatedObject(self, &AssociatedKeys.managerKey, manager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
72 | }
73 | }
74 | }
75 |
76 | // MARK: - ServerTrustPolicy
77 |
78 | /// The `ServerTrustPolicy` evaluates the server trust generally provided by an `NSURLAuthenticationChallenge` when
79 | /// connecting to a server over a secure HTTPS connection. The policy configuration then evaluates the server trust
80 | /// with a given set of criteria to determine whether the server trust is valid and the connection should be made.
81 | ///
82 | /// Using pinned certificates or public keys for evaluation helps prevent man-in-the-middle (MITM) attacks and other
83 | /// vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged
84 | /// to route all communication over an HTTPS connection with pinning enabled.
85 | ///
86 | /// - performDefaultEvaluation: Uses the default server trust evaluation while allowing you to control whether to
87 | /// validate the host provided by the challenge. Applications are encouraged to always
88 | /// validate the host in production environments to guarantee the validity of the server's
89 | /// certificate chain.
90 | ///
91 | /// - performRevokedEvaluation: Uses the default and revoked server trust evaluations allowing you to control whether to
92 | /// validate the host provided by the challenge as well as specify the revocation flags for
93 | /// testing for revoked certificates. Apple platforms did not start testing for revoked
94 | /// certificates automatically until iOS 10.1, macOS 10.12 and tvOS 10.1 which is
95 | /// demonstrated in our TLS tests. Applications are encouraged to always validate the host
96 | /// in production environments to guarantee the validity of the server's certificate chain.
97 | ///
98 | /// - pinCertificates: Uses the pinned certificates to validate the server trust. The server trust is
99 | /// considered valid if one of the pinned certificates match one of the server certificates.
100 | /// By validating both the certificate chain and host, certificate pinning provides a very
101 | /// secure form of server trust validation mitigating most, if not all, MITM attacks.
102 | /// Applications are encouraged to always validate the host and require a valid certificate
103 | /// chain in production environments.
104 | ///
105 | /// - pinPublicKeys: Uses the pinned public keys to validate the server trust. The server trust is considered
106 | /// valid if one of the pinned public keys match one of the server certificate public keys.
107 | /// By validating both the certificate chain and host, public key pinning provides a very
108 | /// secure form of server trust validation mitigating most, if not all, MITM attacks.
109 | /// Applications are encouraged to always validate the host and require a valid certificate
110 | /// chain in production environments.
111 | ///
112 | /// - disableEvaluation: Disables all evaluation which in turn will always consider any server trust as valid.
113 | ///
114 | /// - customEvaluation: Uses the associated closure to evaluate the validity of the server trust.
115 | public enum ServerTrustPolicy {
116 | case performDefaultEvaluation(validateHost: Bool)
117 | case performRevokedEvaluation(validateHost: Bool, revocationFlags: CFOptionFlags)
118 | case pinCertificates(certificates: [SecCertificate], validateCertificateChain: Bool, validateHost: Bool)
119 | case pinPublicKeys(publicKeys: [SecKey], validateCertificateChain: Bool, validateHost: Bool)
120 | case disableEvaluation
121 | case customEvaluation((_ serverTrust: SecTrust, _ host: String) -> Bool)
122 |
123 | // MARK: - Bundle Location
124 |
125 | /// Returns all certificates within the given bundle with a `.cer` file extension.
126 | ///
127 | /// - parameter bundle: The bundle to search for all `.cer` files.
128 | ///
129 | /// - returns: All certificates within the given bundle.
130 | public static func certificates(in bundle: Bundle = Bundle.main) -> [SecCertificate] {
131 | var certificates: [SecCertificate] = []
132 |
133 | let paths = Set([".cer", ".CER", ".crt", ".CRT", ".der", ".DER"].map { fileExtension in
134 | bundle.paths(forResourcesOfType: fileExtension, inDirectory: nil)
135 | }.joined())
136 |
137 | for path in paths {
138 | if
139 | let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData,
140 | let certificate = SecCertificateCreateWithData(nil, certificateData)
141 | {
142 | certificates.append(certificate)
143 | }
144 | }
145 |
146 | return certificates
147 | }
148 |
149 | /// Returns all public keys within the given bundle with a `.cer` file extension.
150 | ///
151 | /// - parameter bundle: The bundle to search for all `*.cer` files.
152 | ///
153 | /// - returns: All public keys within the given bundle.
154 | public static func publicKeys(in bundle: Bundle = Bundle.main) -> [SecKey] {
155 | var publicKeys: [SecKey] = []
156 |
157 | for certificate in certificates(in: bundle) {
158 | if let publicKey = publicKey(for: certificate) {
159 | publicKeys.append(publicKey)
160 | }
161 | }
162 |
163 | return publicKeys
164 | }
165 |
166 | // MARK: - Evaluation
167 |
168 | /// Evaluates whether the server trust is valid for the given host.
169 | ///
170 | /// - parameter serverTrust: The server trust to evaluate.
171 | /// - parameter host: The host of the challenge protection space.
172 | ///
173 | /// - returns: Whether the server trust is valid.
174 | public func evaluate(_ serverTrust: SecTrust, forHost host: String) -> Bool {
175 | var serverTrustIsValid = false
176 |
177 | switch self {
178 | case let .performDefaultEvaluation(validateHost):
179 | let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
180 | SecTrustSetPolicies(serverTrust, policy)
181 |
182 | serverTrustIsValid = trustIsValid(serverTrust)
183 | case let .performRevokedEvaluation(validateHost, revocationFlags):
184 | let defaultPolicy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
185 | let revokedPolicy = SecPolicyCreateRevocation(revocationFlags)
186 | SecTrustSetPolicies(serverTrust, [defaultPolicy, revokedPolicy] as CFTypeRef)
187 |
188 | serverTrustIsValid = trustIsValid(serverTrust)
189 | case let .pinCertificates(pinnedCertificates, validateCertificateChain, validateHost):
190 | if validateCertificateChain {
191 | let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
192 | SecTrustSetPolicies(serverTrust, policy)
193 |
194 | SecTrustSetAnchorCertificates(serverTrust, pinnedCertificates as CFArray)
195 | SecTrustSetAnchorCertificatesOnly(serverTrust, true)
196 |
197 | serverTrustIsValid = trustIsValid(serverTrust)
198 | } else {
199 | let serverCertificatesDataArray = certificateData(for: serverTrust)
200 | let pinnedCertificatesDataArray = certificateData(for: pinnedCertificates)
201 |
202 | outerLoop: for serverCertificateData in serverCertificatesDataArray {
203 | for pinnedCertificateData in pinnedCertificatesDataArray {
204 | if serverCertificateData == pinnedCertificateData {
205 | serverTrustIsValid = true
206 | break outerLoop
207 | }
208 | }
209 | }
210 | }
211 | case let .pinPublicKeys(pinnedPublicKeys, validateCertificateChain, validateHost):
212 | var certificateChainEvaluationPassed = true
213 |
214 | if validateCertificateChain {
215 | let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
216 | SecTrustSetPolicies(serverTrust, policy)
217 |
218 | certificateChainEvaluationPassed = trustIsValid(serverTrust)
219 | }
220 |
221 | if certificateChainEvaluationPassed {
222 | outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] {
223 | for pinnedPublicKey in pinnedPublicKeys as [AnyObject] {
224 | if serverPublicKey.isEqual(pinnedPublicKey) {
225 | serverTrustIsValid = true
226 | break outerLoop
227 | }
228 | }
229 | }
230 | }
231 | case .disableEvaluation:
232 | serverTrustIsValid = true
233 | case let .customEvaluation(closure):
234 | serverTrustIsValid = closure(serverTrust, host)
235 | }
236 |
237 | return serverTrustIsValid
238 | }
239 |
240 | // MARK: - Private - Trust Validation
241 |
242 | private func trustIsValid(_ trust: SecTrust) -> Bool {
243 | var isValid = false
244 |
245 | var result = SecTrustResultType.invalid
246 | let status = SecTrustEvaluate(trust, &result)
247 |
248 | if status == errSecSuccess {
249 | let unspecified = SecTrustResultType.unspecified
250 | let proceed = SecTrustResultType.proceed
251 |
252 |
253 | isValid = result == unspecified || result == proceed
254 | }
255 |
256 | return isValid
257 | }
258 |
259 | // MARK: - Private - Certificate Data
260 |
261 | private func certificateData(for trust: SecTrust) -> [Data] {
262 | var certificates: [SecCertificate] = []
263 |
264 | for index in 0.. [Data] {
274 | return certificates.map { SecCertificateCopyData($0) as Data }
275 | }
276 |
277 | // MARK: - Private - Public Key Extraction
278 |
279 | private static func publicKeys(for trust: SecTrust) -> [SecKey] {
280 | var publicKeys: [SecKey] = []
281 |
282 | for index in 0.. SecKey? {
295 | var publicKey: SecKey?
296 |
297 | let policy = SecPolicyCreateBasicX509()
298 | var trust: SecTrust?
299 | let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &trust)
300 |
301 | if let trust = trust, trustCreationStatus == errSecSuccess {
302 | publicKey = SecTrustCopyPublicKey(trust)
303 | }
304 |
305 | return publicKey
306 | }
307 | }
308 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/TaskDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskDelegate.swift
3 | //
4 | // Copyright (c) 2014-2016 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 | /// The task delegate is responsible for handling all delegate callbacks for the underlying task as well as
28 | /// executing all operations attached to the serial operation queue upon task completion.
29 | open class TaskDelegate: NSObject {
30 |
31 | // MARK: Properties
32 |
33 | /// The serial operation queue used to execute all operations after the task completes.
34 | open let queue: OperationQueue
35 |
36 | /// The data returned by the server.
37 | public var data: Data? { return nil }
38 |
39 | /// The error generated throughout the lifecyle of the task.
40 | public var error: Error?
41 |
42 | var task: URLSessionTask? {
43 | didSet { reset() }
44 | }
45 |
46 | var initialResponseTime: CFAbsoluteTime?
47 | var credential: URLCredential?
48 | var metrics: AnyObject? // URLSessionTaskMetrics
49 |
50 | // MARK: Lifecycle
51 |
52 | init(task: URLSessionTask?) {
53 | self.task = task
54 |
55 | self.queue = {
56 | let operationQueue = OperationQueue()
57 |
58 | operationQueue.maxConcurrentOperationCount = 1
59 | operationQueue.isSuspended = true
60 | operationQueue.qualityOfService = .utility
61 |
62 | return operationQueue
63 | }()
64 | }
65 |
66 | func reset() {
67 | error = nil
68 | initialResponseTime = nil
69 | }
70 |
71 | // MARK: URLSessionTaskDelegate
72 |
73 | var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
74 | var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
75 | var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)?
76 | var taskDidCompleteWithError: ((URLSession, URLSessionTask, Error?) -> Void)?
77 |
78 | @objc(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)
79 | func urlSession(
80 | _ session: URLSession,
81 | task: URLSessionTask,
82 | willPerformHTTPRedirection response: HTTPURLResponse,
83 | newRequest request: URLRequest,
84 | completionHandler: @escaping (URLRequest?) -> Void)
85 | {
86 | var redirectRequest: URLRequest? = request
87 |
88 | if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection {
89 | redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request)
90 | }
91 |
92 | completionHandler(redirectRequest)
93 | }
94 |
95 | @objc(URLSession:task:didReceiveChallenge:completionHandler:)
96 | func urlSession(
97 | _ session: URLSession,
98 | task: URLSessionTask,
99 | didReceive challenge: URLAuthenticationChallenge,
100 | completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
101 | {
102 | var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
103 | var credential: URLCredential?
104 |
105 | if let taskDidReceiveChallenge = taskDidReceiveChallenge {
106 | (disposition, credential) = taskDidReceiveChallenge(session, task, challenge)
107 | } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
108 | let host = challenge.protectionSpace.host
109 |
110 | if
111 | let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host),
112 | let serverTrust = challenge.protectionSpace.serverTrust
113 | {
114 | if serverTrustPolicy.evaluate(serverTrust, forHost: host) {
115 | disposition = .useCredential
116 | credential = URLCredential(trust: serverTrust)
117 | } else {
118 | disposition = .cancelAuthenticationChallenge
119 | }
120 | }
121 | } else {
122 | if challenge.previousFailureCount > 0 {
123 | disposition = .rejectProtectionSpace
124 | } else {
125 | credential = self.credential ?? session.configuration.urlCredentialStorage?.defaultCredential(for: challenge.protectionSpace)
126 |
127 | if credential != nil {
128 | disposition = .useCredential
129 | }
130 | }
131 | }
132 |
133 | completionHandler(disposition, credential)
134 | }
135 |
136 | @objc(URLSession:task:needNewBodyStream:)
137 | func urlSession(
138 | _ session: URLSession,
139 | task: URLSessionTask,
140 | needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
141 | {
142 | var bodyStream: InputStream?
143 |
144 | if let taskNeedNewBodyStream = taskNeedNewBodyStream {
145 | bodyStream = taskNeedNewBodyStream(session, task)
146 | }
147 |
148 | completionHandler(bodyStream)
149 | }
150 |
151 | @objc(URLSession:task:didCompleteWithError:)
152 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
153 | if let taskDidCompleteWithError = taskDidCompleteWithError {
154 | taskDidCompleteWithError(session, task, error)
155 | } else {
156 | if let error = error {
157 | if self.error == nil { self.error = error }
158 |
159 | if
160 | let downloadDelegate = self as? DownloadTaskDelegate,
161 | let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data
162 | {
163 | downloadDelegate.resumeData = resumeData
164 | }
165 | }
166 |
167 | queue.isSuspended = false
168 | }
169 | }
170 | }
171 |
172 | // MARK: -
173 |
174 | class DataTaskDelegate: TaskDelegate, URLSessionDataDelegate {
175 |
176 | // MARK: Properties
177 |
178 | var dataTask: URLSessionDataTask { return task as! URLSessionDataTask }
179 |
180 | override var data: Data? {
181 | if dataStream != nil {
182 | return nil
183 | } else {
184 | return mutableData
185 | }
186 | }
187 |
188 | var progress: Progress
189 | var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
190 |
191 | var dataStream: ((_ data: Data) -> Void)?
192 |
193 | private var totalBytesReceived: Int64 = 0
194 | private var mutableData: Data
195 |
196 | private var expectedContentLength: Int64?
197 |
198 | // MARK: Lifecycle
199 |
200 | override init(task: URLSessionTask?) {
201 | mutableData = Data()
202 | progress = Progress(totalUnitCount: 0)
203 |
204 | super.init(task: task)
205 | }
206 |
207 | override func reset() {
208 | super.reset()
209 |
210 | progress = Progress(totalUnitCount: 0)
211 | totalBytesReceived = 0
212 | mutableData = Data()
213 | expectedContentLength = nil
214 | }
215 |
216 | // MARK: URLSessionDataDelegate
217 |
218 | var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)?
219 | var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)?
220 | var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)?
221 | var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?
222 |
223 | func urlSession(
224 | _ session: URLSession,
225 | dataTask: URLSessionDataTask,
226 | didReceive response: URLResponse,
227 | completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
228 | {
229 | var disposition: URLSession.ResponseDisposition = .allow
230 |
231 | expectedContentLength = response.expectedContentLength
232 |
233 | if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse {
234 | disposition = dataTaskDidReceiveResponse(session, dataTask, response)
235 | }
236 |
237 | completionHandler(disposition)
238 | }
239 |
240 | func urlSession(
241 | _ session: URLSession,
242 | dataTask: URLSessionDataTask,
243 | didBecome downloadTask: URLSessionDownloadTask)
244 | {
245 | dataTaskDidBecomeDownloadTask?(session, dataTask, downloadTask)
246 | }
247 |
248 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
249 | if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
250 |
251 | if let dataTaskDidReceiveData = dataTaskDidReceiveData {
252 | dataTaskDidReceiveData(session, dataTask, data)
253 | } else {
254 | if let dataStream = dataStream {
255 | dataStream(data)
256 | } else {
257 | mutableData.append(data)
258 | }
259 |
260 | let bytesReceived = Int64(data.count)
261 | totalBytesReceived += bytesReceived
262 | let totalBytesExpected = dataTask.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
263 |
264 | progress.totalUnitCount = totalBytesExpected
265 | progress.completedUnitCount = totalBytesReceived
266 |
267 | if let progressHandler = progressHandler {
268 | progressHandler.queue.async { progressHandler.closure(self.progress) }
269 | }
270 | }
271 | }
272 |
273 | func urlSession(
274 | _ session: URLSession,
275 | dataTask: URLSessionDataTask,
276 | willCacheResponse proposedResponse: CachedURLResponse,
277 | completionHandler: @escaping (CachedURLResponse?) -> Void)
278 | {
279 | var cachedResponse: CachedURLResponse? = proposedResponse
280 |
281 | if let dataTaskWillCacheResponse = dataTaskWillCacheResponse {
282 | cachedResponse = dataTaskWillCacheResponse(session, dataTask, proposedResponse)
283 | }
284 |
285 | completionHandler(cachedResponse)
286 | }
287 | }
288 |
289 | // MARK: -
290 |
291 | class DownloadTaskDelegate: TaskDelegate, URLSessionDownloadDelegate {
292 |
293 | // MARK: Properties
294 |
295 | var downloadTask: URLSessionDownloadTask { return task as! URLSessionDownloadTask }
296 |
297 | var progress: Progress
298 | var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
299 |
300 | var resumeData: Data?
301 | override var data: Data? { return resumeData }
302 |
303 | var destination: DownloadRequest.DownloadFileDestination?
304 |
305 | var temporaryURL: URL?
306 | var destinationURL: URL?
307 |
308 | var fileURL: URL? { return destination != nil ? destinationURL : temporaryURL }
309 |
310 | // MARK: Lifecycle
311 |
312 | override init(task: URLSessionTask?) {
313 | progress = Progress(totalUnitCount: 0)
314 | super.init(task: task)
315 | }
316 |
317 | override func reset() {
318 | super.reset()
319 |
320 | progress = Progress(totalUnitCount: 0)
321 | resumeData = nil
322 | }
323 |
324 | // MARK: URLSessionDownloadDelegate
325 |
326 | var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> URL)?
327 | var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
328 | var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)?
329 |
330 | func urlSession(
331 | _ session: URLSession,
332 | downloadTask: URLSessionDownloadTask,
333 | didFinishDownloadingTo location: URL)
334 | {
335 | temporaryURL = location
336 |
337 | guard
338 | let destination = destination,
339 | let response = downloadTask.response as? HTTPURLResponse
340 | else { return }
341 |
342 | let result = destination(location, response)
343 | let destinationURL = result.destinationURL
344 | let options = result.options
345 |
346 | self.destinationURL = destinationURL
347 |
348 | do {
349 | if options.contains(.removePreviousFile), FileManager.default.fileExists(atPath: destinationURL.path) {
350 | try FileManager.default.removeItem(at: destinationURL)
351 | }
352 |
353 | if options.contains(.createIntermediateDirectories) {
354 | let directory = destinationURL.deletingLastPathComponent()
355 | try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
356 | }
357 |
358 | try FileManager.default.moveItem(at: location, to: destinationURL)
359 | } catch {
360 | self.error = error
361 | }
362 | }
363 |
364 | func urlSession(
365 | _ session: URLSession,
366 | downloadTask: URLSessionDownloadTask,
367 | didWriteData bytesWritten: Int64,
368 | totalBytesWritten: Int64,
369 | totalBytesExpectedToWrite: Int64)
370 | {
371 | if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
372 |
373 | if let downloadTaskDidWriteData = downloadTaskDidWriteData {
374 | downloadTaskDidWriteData(
375 | session,
376 | downloadTask,
377 | bytesWritten,
378 | totalBytesWritten,
379 | totalBytesExpectedToWrite
380 | )
381 | } else {
382 | progress.totalUnitCount = totalBytesExpectedToWrite
383 | progress.completedUnitCount = totalBytesWritten
384 |
385 | if let progressHandler = progressHandler {
386 | progressHandler.queue.async { progressHandler.closure(self.progress) }
387 | }
388 | }
389 | }
390 |
391 | func urlSession(
392 | _ session: URLSession,
393 | downloadTask: URLSessionDownloadTask,
394 | didResumeAtOffset fileOffset: Int64,
395 | expectedTotalBytes: Int64)
396 | {
397 | if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
398 | downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
399 | } else {
400 | progress.totalUnitCount = expectedTotalBytes
401 | progress.completedUnitCount = fileOffset
402 | }
403 | }
404 | }
405 |
406 | // MARK: -
407 |
408 | class UploadTaskDelegate: DataTaskDelegate {
409 |
410 | // MARK: Properties
411 |
412 | var uploadTask: URLSessionUploadTask { return task as! URLSessionUploadTask }
413 |
414 | var uploadProgress: Progress
415 | var uploadProgressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
416 |
417 | // MARK: Lifecycle
418 |
419 | override init(task: URLSessionTask?) {
420 | uploadProgress = Progress(totalUnitCount: 0)
421 | super.init(task: task)
422 | }
423 |
424 | override func reset() {
425 | super.reset()
426 | uploadProgress = Progress(totalUnitCount: 0)
427 | }
428 |
429 | // MARK: URLSessionTaskDelegate
430 |
431 | var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)?
432 |
433 | func URLSession(
434 | _ session: URLSession,
435 | task: URLSessionTask,
436 | didSendBodyData bytesSent: Int64,
437 | totalBytesSent: Int64,
438 | totalBytesExpectedToSend: Int64)
439 | {
440 | if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
441 |
442 | if let taskDidSendBodyData = taskDidSendBodyData {
443 | taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
444 | } else {
445 | uploadProgress.totalUnitCount = totalBytesExpectedToSend
446 | uploadProgress.completedUnitCount = totalBytesSent
447 |
448 | if let uploadProgressHandler = uploadProgressHandler {
449 | uploadProgressHandler.queue.async { uploadProgressHandler.closure(self.uploadProgress) }
450 | }
451 | }
452 | }
453 | }
454 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/Timeline.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Timeline.swift
3 | //
4 | // Copyright (c) 2014-2016 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 |
--------------------------------------------------------------------------------
/DADependencyInjection/Libraries/Alamofire/Validation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Validation.swift
3 | //
4 | // Copyright (c) 2014-2016 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 Request {
28 |
29 | // MARK: Helper Types
30 |
31 | fileprivate typealias ErrorReason = AFError.ResponseValidationFailureReason
32 |
33 | /// Used to represent whether validation was successful or encountered an error resulting in a failure.
34 | ///
35 | /// - success: The validation was successful.
36 | /// - failure: The validation failed encountering the provided error.
37 | public enum ValidationResult {
38 | case success
39 | case failure(Error)
40 | }
41 |
42 | fileprivate struct MIMEType {
43 | let type: String
44 | let subtype: String
45 |
46 | var isWildcard: Bool { return type == "*" && subtype == "*" }
47 |
48 | init?(_ string: String) {
49 | let components: [String] = {
50 | let stripped = string.trimmingCharacters(in: .whitespacesAndNewlines)
51 | let split = stripped.substring(to: stripped.range(of: ";")?.lowerBound ?? stripped.endIndex)
52 | return split.components(separatedBy: "/")
53 | }()
54 |
55 | if let type = components.first, let subtype = components.last {
56 | self.type = type
57 | self.subtype = subtype
58 | } else {
59 | return nil
60 | }
61 | }
62 |
63 | func matches(_ mime: MIMEType) -> Bool {
64 | switch (type, subtype) {
65 | case (mime.type, mime.subtype), (mime.type, "*"), ("*", mime.subtype), ("*", "*"):
66 | return true
67 | default:
68 | return false
69 | }
70 | }
71 | }
72 |
73 | // MARK: Properties
74 |
75 | fileprivate var acceptableStatusCodes: [Int] { return Array(200..<300) }
76 |
77 | fileprivate var acceptableContentTypes: [String] {
78 | if let accept = request?.value(forHTTPHeaderField: "Accept") {
79 | return accept.components(separatedBy: ",")
80 | }
81 |
82 | return ["*/*"]
83 | }
84 |
85 | // MARK: Status Code
86 |
87 | fileprivate func validate(
88 | statusCode acceptableStatusCodes: S,
89 | response: HTTPURLResponse)
90 | -> ValidationResult
91 | where S.Iterator.Element == Int
92 | {
93 | if acceptableStatusCodes.contains(response.statusCode) {
94 | return .success
95 | } else {
96 | let reason: ErrorReason = .unacceptableStatusCode(code: response.statusCode)
97 | return .failure(AFError.responseValidationFailed(reason: reason))
98 | }
99 | }
100 |
101 | // MARK: Content Type
102 |
103 | fileprivate func validate(
104 | contentType acceptableContentTypes: S,
105 | response: HTTPURLResponse,
106 | data: Data?)
107 | -> ValidationResult
108 | where S.Iterator.Element == String
109 | {
110 | guard let data = data, data.count > 0 else { return .success }
111 |
112 | guard
113 | let responseContentType = response.mimeType,
114 | let responseMIMEType = MIMEType(responseContentType)
115 | else {
116 | for contentType in acceptableContentTypes {
117 | if let mimeType = MIMEType(contentType), mimeType.isWildcard {
118 | return .success
119 | }
120 | }
121 |
122 | let error: AFError = {
123 | let reason: ErrorReason = .missingContentType(acceptableContentTypes: Array(acceptableContentTypes))
124 | return AFError.responseValidationFailed(reason: reason)
125 | }()
126 |
127 | return .failure(error)
128 | }
129 |
130 | for contentType in acceptableContentTypes {
131 | if let acceptableMIMEType = MIMEType(contentType), acceptableMIMEType.matches(responseMIMEType) {
132 | return .success
133 | }
134 | }
135 |
136 | let error: AFError = {
137 | let reason: ErrorReason = .unacceptableContentType(
138 | acceptableContentTypes: Array(acceptableContentTypes),
139 | responseContentType: responseContentType
140 | )
141 |
142 | return AFError.responseValidationFailed(reason: reason)
143 | }()
144 |
145 | return .failure(error)
146 | }
147 | }
148 |
149 | // MARK: -
150 |
151 | extension DataRequest {
152 | /// A closure used to validate a request that takes a URL request, a URL response and data, and returns whether the
153 | /// request was valid.
154 | public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult
155 |
156 | /// Validates the request, using the specified closure.
157 | ///
158 | /// If validation fails, subsequent calls to response handlers will have an associated error.
159 | ///
160 | /// - parameter validation: A closure to validate the request.
161 | ///
162 | /// - returns: The request.
163 | @discardableResult
164 | public func validate(_ validation: @escaping Validation) -> Self {
165 | let validationExecution: () -> Void = { [unowned self] in
166 | if
167 | let response = self.response,
168 | self.delegate.error == nil,
169 | case let .failure(error) = validation(self.request, response, self.delegate.data)
170 | {
171 | self.delegate.error = error
172 | }
173 | }
174 |
175 | validations.append(validationExecution)
176 |
177 | return self
178 | }
179 |
180 | /// Validates that the response has a status code in the specified sequence.
181 | ///
182 | /// If validation fails, subsequent calls to response handlers will have an associated error.
183 | ///
184 | /// - parameter range: The range of acceptable status codes.
185 | ///
186 | /// - returns: The request.
187 | @discardableResult
188 | public func validate(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
189 | return validate { [unowned self] _, response, _ in
190 | return self.validate(statusCode: acceptableStatusCodes, response: response)
191 | }
192 | }
193 |
194 | /// Validates that the response has a content type in the specified sequence.
195 | ///
196 | /// If validation fails, subsequent calls to response handlers will have an associated error.
197 | ///
198 | /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
199 | ///
200 | /// - returns: The request.
201 | @discardableResult
202 | public func validate(contentType acceptableContentTypes: S) -> Self where S.Iterator.Element == String {
203 | return validate { [unowned self] _, response, data in
204 | return self.validate(contentType: acceptableContentTypes, response: response, data: data)
205 | }
206 | }
207 |
208 | /// Validates that the response has a status code in the default acceptable range of 200...299, and that the content
209 | /// type matches any specified in the Accept HTTP header field.
210 | ///
211 | /// If validation fails, subsequent calls to response handlers will have an associated error.
212 | ///
213 | /// - returns: The request.
214 | @discardableResult
215 | public func validate() -> Self {
216 | return validate(statusCode: self.acceptableStatusCodes).validate(contentType: self.acceptableContentTypes)
217 | }
218 | }
219 |
220 | // MARK: -
221 |
222 | extension DownloadRequest {
223 | /// A closure used to validate a request that takes a URL request, a URL response, a temporary URL and a
224 | /// destination URL, and returns whether the request was valid.
225 | public typealias Validation = (
226 | _ request: URLRequest?,
227 | _ response: HTTPURLResponse,
228 | _ temporaryURL: URL?,
229 | _ destinationURL: URL?)
230 | -> ValidationResult
231 |
232 | /// Validates the request, using the specified closure.
233 | ///
234 | /// If validation fails, subsequent calls to response handlers will have an associated error.
235 | ///
236 | /// - parameter validation: A closure to validate the request.
237 | ///
238 | /// - returns: The request.
239 | @discardableResult
240 | public func validate(_ validation: @escaping Validation) -> Self {
241 | let validationExecution: () -> Void = { [unowned self] in
242 | let request = self.request
243 | let temporaryURL = self.downloadDelegate.temporaryURL
244 | let destinationURL = self.downloadDelegate.destinationURL
245 |
246 | if
247 | let response = self.response,
248 | self.delegate.error == nil,
249 | case let .failure(error) = validation(request, response, temporaryURL, destinationURL)
250 | {
251 | self.delegate.error = error
252 | }
253 | }
254 |
255 | validations.append(validationExecution)
256 |
257 | return self
258 | }
259 |
260 | /// Validates that the response has a status code in the specified sequence.
261 | ///
262 | /// If validation fails, subsequent calls to response handlers will have an associated error.
263 | ///
264 | /// - parameter range: The range of acceptable status codes.
265 | ///
266 | /// - returns: The request.
267 | @discardableResult
268 | public func validate(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
269 | return validate { [unowned self] _, response, _, _ in
270 | return self.validate(statusCode: acceptableStatusCodes, response: response)
271 | }
272 | }
273 |
274 | /// Validates that the response has a content type in the specified sequence.
275 | ///
276 | /// If validation fails, subsequent calls to response handlers will have an associated error.
277 | ///
278 | /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
279 | ///
280 | /// - returns: The request.
281 | @discardableResult
282 | public func validate(contentType acceptableContentTypes: S) -> Self where S.Iterator.Element == String {
283 | return validate { [unowned self] _, response, _, _ in
284 | let fileURL = self.downloadDelegate.fileURL
285 |
286 | guard let validFileURL = fileURL else {
287 | return .failure(AFError.responseValidationFailed(reason: .dataFileNil))
288 | }
289 |
290 | do {
291 | let data = try Data(contentsOf: validFileURL)
292 | return self.validate(contentType: acceptableContentTypes, response: response, data: data)
293 | } catch {
294 | return .failure(AFError.responseValidationFailed(reason: .dataFileReadFailed(at: validFileURL)))
295 | }
296 | }
297 | }
298 |
299 | /// Validates that the response has a status code in the default acceptable range of 200...299, and that the content
300 | /// type matches any specified in the Accept HTTP header field.
301 | ///
302 | /// If validation fails, subsequent calls to response handlers will have an associated error.
303 | ///
304 | /// - returns: The request.
305 | @discardableResult
306 | public func validate() -> Self {
307 | return validate(statusCode: self.acceptableStatusCodes).validate(contentType: self.acceptableContentTypes)
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/DADependencyInjection/SiriConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SiriConstants.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 21/05/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum SiriConstants: String {
12 | case ItemTitle
13 | case ItemDescription
14 | }
15 |
--------------------------------------------------------------------------------
/DADependencyInjection/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 19/02/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Intents
11 |
12 | private enum Constants: String {
13 | case cellID = "ListItemCell"
14 | }
15 |
16 | class ViewController: UIViewController {
17 |
18 | fileprivate var items: [ListDisplayable]?
19 | var dataProvider: ListDisplayableDataProvider = MoviesManager()
20 |
21 | @IBOutlet weak var tableView: UITableView!
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 |
26 | self.setupRefreshControl()
27 | self.requestSiriAuthorization()
28 |
29 | self.reloadData()
30 | }
31 |
32 | private func setupRefreshControl() {
33 | self.tableView.refreshControl = UIRefreshControl()
34 | self.tableView.refreshControl?.addTarget(self, action: #selector(ViewController.refreshControlAction), for: UIControlEvents.valueChanged)
35 | }
36 |
37 | private func requestSiriAuthorization() {
38 | INPreferences.requestSiriAuthorization { (status) in
39 | switch status {
40 | case .authorized:
41 | print("authorized")
42 | case .denied:
43 | print("denied")
44 | case .notDetermined:
45 | print("notDetermined")
46 | case .restricted:
47 | print("restricted")
48 | }
49 | }
50 | }
51 |
52 | func refreshControlAction() {
53 | self.reloadData()
54 | }
55 |
56 | private func reloadData() {
57 | self.tableView.refreshControl?.beginRefreshing()
58 | dataProvider.getListItems { (items) in
59 | self.tableView.refreshControl?.endRefreshing()
60 | self.items = items
61 | self.tableView.reloadData()
62 | }
63 | }
64 |
65 | @IBAction private func clearAllData() {
66 | self.items = nil
67 | self.tableView.reloadData()
68 | }
69 | }
70 |
71 | extension ViewController: UITableViewDataSource {
72 |
73 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
74 | return items?.count ?? 0
75 | }
76 |
77 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
78 | let cell = tableView.dequeueReusableCell(withIdentifier: Constants.cellID.rawValue)
79 |
80 | if let cellItem = items?[indexPath.row] {
81 | cell?.textLabel?.text = cellItem.listItemTitle
82 | cell?.detailTextLabel?.text = cellItem.listItemSubtitle
83 | }
84 |
85 | return cell!
86 | }
87 | }
88 |
89 | // MARK: Just for testing
90 | extension ViewController {
91 |
92 | // This method is not part of the pattern, we'll use it for testing only
93 | @IBAction func segmentedControlAction(_ sender: UISegmentedControl) {
94 |
95 | guard let moviesManager = dataProvider as? MoviesManager else {
96 | return
97 | }
98 |
99 | if sender.selectedSegmentIndex == 0 {
100 | moviesManager.moviesDataProvider = MoviesDataSource()
101 | moviesManager.moviesDataProvider.networkingProvider = AFNetworkConnector()
102 | } else if sender.selectedSegmentIndex == 1 {
103 | moviesManager.moviesDataProvider = MoviesDataSource()
104 | moviesManager.moviesDataProvider.networkingProvider = NSURLNetworkConnector()
105 | } else if sender.selectedSegmentIndex == 2 {
106 | moviesManager.moviesDataProvider = MoviesDataSource_Operations()
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/DADependencyInjectionTests/Data/TestData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestData.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 19/04/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol TestDataProvider {
12 | init(withFileName fileName: String)
13 | var dataRepresentation: Data? { get }
14 | var jsonRepresentation: Any? { get }
15 | }
16 |
17 | class TestData {
18 |
19 | fileprivate let fileName: NSString
20 |
21 | required init(withFileName fileName: String) {
22 | self.fileName = fileName as NSString
23 | }
24 | }
25 |
26 | // MARK: TestDataSource
27 | extension TestData: TestDataProvider {
28 | public var dataRepresentation: Data? {
29 | get {
30 | let testBundle = Bundle(for: type(of: self))
31 | let filePath = testBundle.path(forResource: fileName.deletingPathExtension, ofType: fileName.pathExtension)
32 |
33 | if let path = filePath {
34 | let result = FileManager.default.contents(atPath: path)
35 | return result
36 | }
37 |
38 | return nil
39 | }
40 | }
41 |
42 | public var jsonRepresentation: Any? {
43 | get {
44 | guard
45 | let data = dataRepresentation,
46 | let jsonObject = try? JSONSerialization.jsonObject(with: data, options: [JSONSerialization.ReadingOptions.allowFragments]) else {
47 | return nil
48 | }
49 |
50 | return jsonObject
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/DADependencyInjectionTests/Data/TestFileNames.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestFiles.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 19/04/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct TestFileNames {
12 | static let PopularMovies = "popularMovies.json"
13 | }
14 |
--------------------------------------------------------------------------------
/DADependencyInjectionTests/Data/popularMovies.json:
--------------------------------------------------------------------------------
1 | {
2 | "page":1,
3 | "results":[
4 | {
5 | "poster_path":"\/tWqifoYuwLETmmasnGHO7xBjEtt.jpg",
6 | "adult":false,
7 | "overview":"A live-action adaptation of Disney's version of the classic 'Beauty and the Beast' tale of a cursed prince and a beautiful young woman who helps him break the spell.",
8 | "release_date":"2017-03-16",
9 | "genre_ids":[
10 | 14,
11 | 10749
12 | ],
13 | "id":321612,
14 | "original_title":"Beauty and the Beast",
15 | "original_language":"en",
16 | "title":"Beauty and the Beast",
17 | "backdrop_path":"\/6aUWe0GSl69wMTSWWexsorMIvwU.jpg",
18 | "popularity":158.103128,
19 | "vote_count":1824,
20 | "video":false,
21 | "vote_average":6.9
22 | },
23 | {
24 | "poster_path":"\/unPB1iyEeTBcKiLg8W083rlViFH.jpg",
25 | "adult":false,
26 | "overview":"A story about how a new baby's arrival impacts a family, told from the point of view of a delightfully unreliable narrator, a wildly imaginative 7 year old named Tim.",
27 | "release_date":"2017-03-23",
28 | "genre_ids":[
29 | 16,
30 | 35,
31 | 10751
32 | ],
33 | "id":295693,
34 | "original_title":"The Boss Baby",
35 | "original_language":"en",
36 | "title":"The Boss Baby",
37 | "backdrop_path":"\/bTFeSwh07oX99ofpDI4O2WkiFJ.jpg",
38 | "popularity":122.933756,
39 | "vote_count":551,
40 | "video":false,
41 | "vote_average":5.7
42 | },
43 | {
44 | "poster_path":"\/45Y1G5FEgttPAwjTYic6czC9xCn.jpg",
45 | "adult":false,
46 | "overview":"In the near future, a weary Logan cares for an ailing Professor X in a hide out on the Mexican border. But Logan's attempts to hide from the world and his legacy are up-ended when a young mutant arrives, being pursued by dark forces.",
47 | "release_date":"2017-02-28",
48 | "genre_ids":[
49 | 28,
50 | 18,
51 | 878
52 | ],
53 | "id":263115,
54 | "original_title":"Logan",
55 | "original_language":"en",
56 | "title":"Logan",
57 | "backdrop_path":"\/5pAGnkFYSsFJ99ZxDIYnhQbQFXs.jpg",
58 | "popularity":85.452554,
59 | "vote_count":2491,
60 | "video":false,
61 | "vote_average":7.5
62 | },
63 | {
64 | "poster_path":"\/s9ye87pvq2IaDvjv9x4IOXVjvA7.jpg",
65 | "adult":false,
66 | "overview":"A koala named Buster recruits his best friend to help him drum up business for his theater by hosting a singing competition.",
67 | "release_date":"2016-11-23",
68 | "genre_ids":[
69 | 16,
70 | 35,
71 | 18,
72 | 10751,
73 | 10402
74 | ],
75 | "id":335797,
76 | "original_title":"Sing",
77 | "original_language":"en",
78 | "title":"Sing",
79 | "backdrop_path":"\/fxDXp8un4qNY9b1dLd7SH6CKzC.jpg",
80 | "popularity":81.440749,
81 | "vote_count":1192,
82 | "video":false,
83 | "vote_average":6.7
84 | },
85 | {
86 | "poster_path":"\/r2517Vz9EhDhj88qwbDVj8DCRZN.jpg",
87 | "adult":false,
88 | "overview":"Explore the mysterious and dangerous home of the king of the apes as a team of explorers ventures deep inside the treacherous, primordial island.",
89 | "release_date":"2017-03-08",
90 | "genre_ids":[
91 | 28,
92 | 12,
93 | 14
94 | ],
95 | "id":293167,
96 | "original_title":"Kong: Skull Island",
97 | "original_language":"en",
98 | "title":"Kong: Skull Island",
99 | "backdrop_path":"\/pGwChWiAY1bdoxL79sXmaFBlYJH.jpg",
100 | "popularity":61.814365,
101 | "vote_count":1241,
102 | "video":false,
103 | "vote_average":6
104 | },
105 | {
106 | "poster_path":"\/iNpz2DgTsTMPaDRZq2tnbqjL2vF.jpg",
107 | "adult":false,
108 | "overview":"When a mysterious woman seduces Dom into the world of crime and a betrayal of those closest to him, the crew face trials that will test them as never before.",
109 | "release_date":"2017-04-12",
110 | "genre_ids":[
111 | 28,
112 | 80,
113 | 53
114 | ],
115 | "id":337339,
116 | "original_title":"The Fate of the Furious",
117 | "original_language":"en",
118 | "title":"The Fate of the Furious",
119 | "backdrop_path":"\/jzdnhRhG0dsuYorwvSqPqqnM1cV.jpg",
120 | "popularity":54.434998,
121 | "vote_count":578,
122 | "video":false,
123 | "vote_average":7.1
124 | },
125 | {
126 | "poster_path":"\/myRzRzCxdfUWjkJWgpHHZ1oGkJd.jpg",
127 | "adult":false,
128 | "overview":"In the near future, Major is the first of her kind: a human saved from a terrible crash, who is cyber-enhanced to be a perfect soldier devoted to stopping the world's most dangerous criminals.",
129 | "release_date":"2017-03-29",
130 | "genre_ids":[
131 | 28,
132 | 18,
133 | 878
134 | ],
135 | "id":315837,
136 | "original_title":"Ghost in the Shell",
137 | "original_language":"en",
138 | "title":"Ghost in the Shell",
139 | "backdrop_path":"\/lsRhmB7m36pEX0UHpkpJSE48BW5.jpg",
140 | "popularity":52.273404,
141 | "vote_count":616,
142 | "video":false,
143 | "vote_average":6
144 | },
145 | {
146 | "poster_path":"\/jjBgi2r5cRt36xF6iNUEhzscEcb.jpg",
147 | "adult":false,
148 | "overview":"Twenty-two years after the events of Jurassic Park, Isla Nublar now features a fully functioning dinosaur theme park, Jurassic World, as originally envisioned by John Hammond.",
149 | "release_date":"2015-06-09",
150 | "genre_ids":[
151 | 28,
152 | 12,
153 | 878,
154 | 53
155 | ],
156 | "id":135397,
157 | "original_title":"Jurassic World",
158 | "original_language":"en",
159 | "title":"Jurassic World",
160 | "backdrop_path":"\/dkMD5qlogeRMiEixC4YNPUvax2T.jpg",
161 | "popularity":47.726583,
162 | "vote_count":6725,
163 | "video":false,
164 | "vote_average":6.5
165 | },
166 | {
167 | "poster_path":"\/rXMWOZiCt6eMX22jWuTOSdQ98bY.jpg",
168 | "adult":false,
169 | "overview":"Though Kevin has evidenced 23 personalities to his trusted psychiatrist, Dr. Fletcher, there remains one still submerged who is set to materialize and dominate all the others. Compelled to abduct three teenage girls led by the willful, observant Casey, Kevin reaches a war for survival among all of those contained within him — as well as everyone around him — as the walls between his compartments shatter apart.",
170 | "release_date":"2016-11-15",
171 | "genre_ids":[
172 | 27,
173 | 53
174 | ],
175 | "id":381288,
176 | "original_title":"Split",
177 | "original_language":"en",
178 | "title":"Split",
179 | "backdrop_path":"\/4G6FNNLSIVrwSRZyFs91hQ3lZtD.jpg",
180 | "popularity":38.459995,
181 | "vote_count":1918,
182 | "video":false,
183 | "vote_average":6.8
184 | },
185 | {
186 | "poster_path":"\/gri0DDxsERr6B2sOR1fGLxLpSLx.jpg",
187 | "adult":false,
188 | "overview":"In 1926, Newt Scamander arrives at the Magical Congress of the United States of America with a magically expanded briefcase, which houses a number of dangerous creatures and their habitats. When the creatures escape from the briefcase, it sends the American wizarding authorities after Newt, and threatens to strain even further the state of magical and non-magical relations.",
189 | "release_date":"2016-11-16",
190 | "genre_ids":[
191 | 12,
192 | 28,
193 | 14
194 | ],
195 | "id":259316,
196 | "original_title":"Fantastic Beasts and Where to Find Them",
197 | "original_language":"en",
198 | "title":"Fantastic Beasts and Where to Find Them",
199 | "backdrop_path":"\/kMzU4PkXcKcDMngCxXji0BbVXsu.jpg",
200 | "popularity":34.726744,
201 | "vote_count":3375,
202 | "video":false,
203 | "vote_average":7
204 | },
205 | {
206 | "poster_path":"\/kqjL17yufvn9OVLyXYpvtyrFfak.jpg",
207 | "adult":false,
208 | "overview":"An apocalyptic story set in the furthest reaches of our planet, in a stark desert landscape where humanity is broken, and most everyone is crazed fighting for the necessities of life. Within this world exist two rebels on the run who just might be able to restore order. There's Max, a man of action and a man of few words, who seeks peace of mind following the loss of his wife and child in the aftermath of the chaos. And Furiosa, a woman of action and a woman who believes her path to survival may be achieved if she can make it across the desert back to her childhood homeland.",
209 | "release_date":"2015-05-13",
210 | "genre_ids":[
211 | 28,
212 | 12,
213 | 878,
214 | 53
215 | ],
216 | "id":76341,
217 | "original_title":"Mad Max: Fury Road",
218 | "original_language":"en",
219 | "title":"Mad Max: Fury Road",
220 | "backdrop_path":"\/phszHPFVhPHhMZgo0fWTKBDQsJA.jpg",
221 | "popularity":30.32121,
222 | "vote_count":7538,
223 | "video":false,
224 | "vote_average":7.2
225 | },
226 | {
227 | "poster_path":"\/y31QB9kn3XSudA15tV7UWQ9XLuW.jpg",
228 | "adult":false,
229 | "overview":"Light years from Earth, 26 years after being abducted, Peter Quill finds himself the prime target of a manhunt after discovering an orb wanted by Ronan the Accuser.",
230 | "release_date":"2014-07-30",
231 | "genre_ids":[
232 | 28,
233 | 878,
234 | 12
235 | ],
236 | "id":118340,
237 | "original_title":"Guardians of the Galaxy",
238 | "original_language":"en",
239 | "title":"Guardians of the Galaxy",
240 | "backdrop_path":"\/bHarw8xrmQeqf3t8HpuMY7zoK4x.jpg",
241 | "popularity":30.261692,
242 | "vote_count":6888,
243 | "video":false,
244 | "vote_average":7.9
245 | },
246 | {
247 | "poster_path":"\/qjiskwlV1qQzRCjpV0cL9pEMF9a.jpg",
248 | "adult":false,
249 | "overview":"A rogue band of resistance fighters unite for a mission to steal the Death Star plans and bring a new hope to the galaxy.",
250 | "release_date":"2016-12-14",
251 | "genre_ids":[
252 | 28,
253 | 18,
254 | 878,
255 | 10752
256 | ],
257 | "id":330459,
258 | "original_title":"Rogue One: A Star Wars Story",
259 | "original_language":"en",
260 | "title":"Rogue One: A Star Wars Story",
261 | "backdrop_path":"\/tZjVVIYXACV4IIIhXeIM59ytqwS.jpg",
262 | "popularity":29.993022,
263 | "vote_count":3187,
264 | "video":false,
265 | "vote_average":7.3
266 | },
267 | {
268 | "poster_path":"\/nBNZadXqJSdt05SHLqgT0HuC5Gm.jpg",
269 | "adult":false,
270 | "overview":"Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.",
271 | "release_date":"2014-11-05",
272 | "genre_ids":[
273 | 12,
274 | 18,
275 | 878
276 | ],
277 | "id":157336,
278 | "original_title":"Interstellar",
279 | "original_language":"en",
280 | "title":"Interstellar",
281 | "backdrop_path":"\/xu9zaAevzQ5nnrsXN6JcahLnG4i.jpg",
282 | "popularity":29.128103,
283 | "vote_count":8189,
284 | "video":false,
285 | "vote_average":8
286 | },
287 | {
288 | "poster_path":"\/hLudzvGfpi6JlwUnsNhXwKKg4j.jpg",
289 | "adult":false,
290 | "overview":"Taking place after alien crafts land around the world, an expert linguist is recruited by the military to determine whether they come in peace or are a threat.",
291 | "release_date":"2016-11-10",
292 | "genre_ids":[
293 | 53,
294 | 18,
295 | 878,
296 | 9648
297 | ],
298 | "id":329865,
299 | "original_title":"Arrival",
300 | "original_language":"en",
301 | "title":"Arrival",
302 | "backdrop_path":"\/yIZ1xendyqKvY3FGeeUYUd5X9Mm.jpg",
303 | "popularity":29.093654,
304 | "vote_count":3572,
305 | "video":false,
306 | "vote_average":6.9
307 | },
308 | {
309 | "poster_path":"\/5vHssUeVe25bMrof1HyaPyWgaP.jpg",
310 | "adult":false,
311 | "overview":"Ex-hitman John Wick comes out of retirement to track down the gangsters that took everything from him.",
312 | "release_date":"2014-10-22",
313 | "genre_ids":[
314 | 28,
315 | 53
316 | ],
317 | "id":245891,
318 | "original_title":"John Wick",
319 | "original_language":"en",
320 | "title":"John Wick",
321 | "backdrop_path":"\/mFb0ygcue4ITixDkdr7wm1Tdarx.jpg",
322 | "popularity":25.194248,
323 | "vote_count":3723,
324 | "video":false,
325 | "vote_average":7
326 | },
327 | {
328 | "poster_path":"\/inVq3FRqcYIRl2la8iZikYYxFNR.jpg",
329 | "adult":false,
330 | "overview":"Based upon Marvel Comics’ most unconventional anti-hero, DEADPOOL tells the origin story of former Special Forces operative turned mercenary Wade Wilson, who after being subjected to a rogue experiment that leaves him with accelerated healing powers, adopts the alter ego Deadpool. Armed with his new abilities and a dark, twisted sense of humor, Deadpool hunts down the man who nearly destroyed his life.",
331 | "release_date":"2016-02-09",
332 | "genre_ids":[
333 | 28,
334 | 12,
335 | 35,
336 | 10749
337 | ],
338 | "id":293660,
339 | "original_title":"Deadpool",
340 | "original_language":"en",
341 | "title":"Deadpool",
342 | "backdrop_path":"\/n1y094tVDFATSzkTnFxoGZ1qNsG.jpg",
343 | "popularity":24.94861,
344 | "vote_count":7846,
345 | "video":false,
346 | "vote_average":7.3
347 | },
348 | {
349 | "poster_path":"\/z09QAf8WbZncbitewNk6lKYMZsh.jpg",
350 | "adult":false,
351 | "overview":"Dory is reunited with her friends Nemo and Marlin in the search for answers about her past. What can she remember? Who are her parents? And where did she learn to speak Whale?",
352 | "release_date":"2016-06-16",
353 | "genre_ids":[
354 | 12,
355 | 16,
356 | 35,
357 | 10751
358 | ],
359 | "id":127380,
360 | "original_title":"Finding Dory",
361 | "original_language":"en",
362 | "title":"Finding Dory",
363 | "backdrop_path":"\/iWRKYHTFlsrxQtfQqFOQyceL83P.jpg",
364 | "popularity":24.257693,
365 | "vote_count":3000,
366 | "video":false,
367 | "vote_average":6.7
368 | },
369 | {
370 | "poster_path":"\/aqiK5aOZmxvtLcY04B1fFeEJLV1.jpg",
371 | "adult":false,
372 | "overview":"In the fourth installment of the fighting franchise, Boyka is shooting for the big leagues when an accidental death in the ring makes him question everything he stands for. When he finds out the wife of the man he accidentally killed is in trouble, Boyka offers to fight in a series of impossible battles to free her from a life of servitude",
373 | "release_date":"2016-09-22",
374 | "genre_ids":[
375 | 28
376 | ],
377 | "id":348893,
378 | "original_title":"Boyka: Undisputed IV",
379 | "original_language":"en",
380 | "title":"Boyka: Undisputed IV",
381 | "backdrop_path":"\/cGc5jUgAvcmA9kBCvtNDvlT4LpR.jpg",
382 | "popularity":24.256069,
383 | "vote_count":87,
384 | "video":false,
385 | "vote_average":5.9
386 | },
387 | {
388 | "poster_path":"\/ylXCdC106IKiarftHkcacasaAcb.jpg",
389 | "adult":false,
390 | "overview":"Mia, an aspiring actress, serves lattes to movie stars in between auditions and Sebastian, a jazz musician, scrapes by playing cocktail party gigs in dingy bars, but as success mounts they are faced with decisions that begin to fray the fragile fabric of their love affair, and the dreams they worked so hard to maintain in each other threaten to rip them apart.",
391 | "release_date":"2016-09-12",
392 | "genre_ids":[
393 | 35,
394 | 18,
395 | 10402,
396 | 10749
397 | ],
398 | "id":313369,
399 | "original_title":"La La Land",
400 | "original_language":"en",
401 | "title":"La La Land",
402 | "backdrop_path":"\/fp6X6yhgcxzxCpmM0EVC6V9B8XB.jpg",
403 | "popularity":22.160501,
404 | "vote_count":2579,
405 | "video":false,
406 | "vote_average":7.9
407 | }
408 | ],
409 | "total_results":19554,
410 | "total_pages":978
411 | }
--------------------------------------------------------------------------------
/DADependencyInjectionTests/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 |
--------------------------------------------------------------------------------
/DADependencyInjectionTests/MockObjects/MockNetworkProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockNetworkProvider.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 19/04/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | @testable import DADependencyInjection
11 |
12 | class MockNetworkProvider {
13 |
14 | fileprivate let dataProvider: TestDataProvider
15 |
16 | init(withDataProvider dataProvider: TestDataProvider) {
17 | self.dataProvider = dataProvider
18 | }
19 | }
20 |
21 | // MARK: NetworkingProvider
22 | extension MockNetworkProvider: NetworkingProvider {
23 | func restCall(urlString: String, onCompleted: ((Data?) -> ())?) {
24 | onCompleted?(self.dataProvider.dataRepresentation)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/DADependencyInjectionTests/MoviesDataSourceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MoviesDataSourceTests.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 19/04/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import DADependencyInjection
11 |
12 | class MoviesDataSourceTests: XCTestCase {
13 |
14 | func testGetMovies_Normal_ListOfMovies() {
15 | let testDataProvider = TestData(withFileName: TestFileNames.PopularMovies)
16 | let networkingProvider = MockNetworkProvider(withDataProvider: testDataProvider)
17 | let dataSource = MoviesDataSource(withNetworkingProvider: networkingProvider, andFactory: JSONMoviesFactory())
18 | var movies: [MovieItem]?
19 |
20 | let getMoviesExpectation = self.expectation(description: "Get Movies Expectation")
21 | dataSource.getMovies { (items) in
22 | movies = items
23 | getMoviesExpectation.fulfill()
24 | }
25 | self.waitForExpectations(timeout: 0.1) { (error) in
26 | guard error == nil else {
27 | XCTFail("Expectation error: \(String(describing: error?.localizedDescription))")
28 | return
29 | }
30 |
31 | XCTAssertNotNil(movies)
32 | XCTAssertTrue(movies?.count == 100) // Operations demo
33 |
34 | let firstMovie = movies?.sorted(){ $0.title > $1.title }.first
35 | XCTAssertEqual(firstMovie?.title, "The Fate of the Furious")
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/DADependencyInjectionTests/MoviesFactoryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MoviesFactoryTests.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 19/04/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import DADependencyInjection
11 |
12 | class MoviesFactoryTests: XCTestCase {
13 |
14 | func testMovieItems_Normal_ItemsArray() {
15 | let testDataProvider = TestData(withFileName: TestFileNames.PopularMovies)
16 |
17 | guard let testJSON = testDataProvider.jsonRepresentation else {
18 | XCTFail("Failed creating a JSON")
19 | return
20 | }
21 |
22 | let moviesFactory = JSONMoviesFactory()
23 | let movies = moviesFactory.movieItems(withJSON: testJSON)
24 |
25 | XCTAssertTrue(movies.count == 20)
26 |
27 | let firstMovie = movies.sorted(){ $0.title > $1.title }.first
28 | XCTAssertEqual(firstMovie?.title, "The Fate of the Furious")
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/DADependencyInjectionTests/MoviesManagerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MoviesManagerTests.swift
3 | // DADependencyInjection
4 | //
5 | // Created by Dejan on 19/04/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import DADependencyInjection
11 |
12 | class MoviesManagerTests: XCTestCase {
13 |
14 | private func createManager() -> MoviesManager {
15 | let networkProvider = MockNetworkProvider(withDataProvider: TestData(withFileName: TestFileNames.PopularMovies))
16 | let dataProvider = MoviesDataSource(withNetworkingProvider: networkProvider, andFactory: JSONMoviesFactory())
17 | return MoviesManager(withDataProvider: dataProvider)
18 | }
19 |
20 | func testGetListItemsDisplayable_Normal_ListItemsArray() {
21 | let manager = createManager()
22 |
23 | var result: [ListDisplayable]?
24 |
25 | let getItemsExpectation = self.expectation(description: "Get List Items Displayable Expectation")
26 | manager.getListItems { (items) in
27 | result = items
28 | getItemsExpectation.fulfill()
29 | }
30 | self.waitForExpectations(timeout: 0.1) { (error) in
31 | guard error == nil else {
32 | XCTFail("Expectation error: \(String(describing: error?.localizedDescription))")
33 | return
34 | }
35 |
36 | XCTAssertNotNil(result)
37 |
38 | XCTAssertTrue(result?.count == 100) // Operations demo
39 |
40 | let firstItem = result?.sorted(){ $0.listItemTitle > $1.listItemTitle }.first
41 | XCTAssertEqual(firstItem?.listItemTitle, "The Fate of the Furious")
42 | XCTAssertEqual(firstItem?.listItemSubtitle, "When a mysterious woman seduces Dom into the world of crime and a betrayal of those closest to him, the crew face trials that will test them as never before.")
43 | }
44 | }
45 |
46 | func testSearchListItems_NotCached() {
47 | let manager = createManager()
48 |
49 | var result: [ListDisplayable]?
50 |
51 | let searchItemsExpectation = self.expectation(description: "Search List Items Displayable Expectation")
52 | manager.searchListItems(searchTerm: "ghost") { (items) in
53 | result = items
54 | searchItemsExpectation.fulfill()
55 | }
56 | self.waitForExpectations(timeout: 0.1) { (error) in
57 | guard error == nil else {
58 | XCTFail("Expectation error: \(String(describing: error?.localizedDescription))")
59 | return
60 | }
61 |
62 | XCTAssertNotNil(result)
63 |
64 | XCTAssertTrue(result?.count == 5) // Operations demo
65 |
66 | let item = result?.first
67 | XCTAssertEqual(item?.listItemTitle, "Ghost in the Shell")
68 | }
69 | }
70 |
71 | func testSearchListItems_Cached() {
72 | let manager = createManager()
73 |
74 | var result: [ListDisplayable]?
75 |
76 | let getItemsExpectation = self.expectation(description: "Get List Items Displayable Expectation")
77 | manager.getListItems { (items) in
78 | result = items
79 | getItemsExpectation.fulfill()
80 | }
81 | self.waitForExpectations(timeout: 0.1) { (error) in
82 | guard error == nil else {
83 | XCTFail("Expectation error: \(String(describing: error?.localizedDescription))")
84 | return
85 | }
86 |
87 | XCTAssertNotNil(result)
88 | }
89 |
90 | let searchItemsExpectation = self.expectation(description: "Search List Items Displayable Expectation")
91 | manager.searchListItems(searchTerm: "ghost") { (items) in
92 | result = items
93 | searchItemsExpectation.fulfill()
94 | }
95 | self.waitForExpectations(timeout: 0.1) { (error) in
96 | guard error == nil else {
97 | XCTFail("Expectation error: \(String(describing: error?.localizedDescription))")
98 | return
99 | }
100 |
101 | XCTAssertNotNil(result)
102 |
103 | XCTAssertTrue(result?.count == 5) // Operations demo
104 |
105 | let item = result?.first
106 | XCTAssertEqual(item?.listItemTitle, "Ghost in the Shell")
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DADependencyInjection [](https://travis-ci.org/dagostini/DADependencyInjection) [](https://codecov.io/gh/dagostini/DADependencyInjection) [](https://codebeat.co/projects/github-com-dagostini-dadependencyinjection-master)
2 |
3 | Example project for blog post at: http://agostini.tech/2017/03/27/using-dependency-injection/
4 |
--------------------------------------------------------------------------------
/SiriTest/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | SiriTest
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | XPC!
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | NSExtension
24 |
25 | NSExtensionAttributes
26 |
27 | IntentsRestrictedWhileLocked
28 |
29 | INSearchForPhotosIntent
30 |
31 | IntentsSupported
32 |
33 | INSearchForPhotosIntent
34 |
35 |
36 | NSExtensionPointIdentifier
37 | com.apple.intents-service
38 | NSExtensionPrincipalClass
39 | $(PRODUCT_MODULE_NAME).IntentHandler
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/SiriTest/IntentHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IntentHandler.swift
3 | // SiriTest
4 | //
5 | // Created by Dejan on 18/05/2017.
6 | // Copyright © 2017 Dejan. All rights reserved.
7 | //
8 |
9 | import Intents
10 |
11 | // As an example, this class is set up to handle Message intents.
12 | // You will want to replace this or add other intents as appropriate.
13 | // The intents you wish to handle must be declared in the extension's Info.plist.
14 |
15 | // You can test your example integration by saying things to Siri like:
16 | // "Send a message using "
17 | // " John saying hello"
18 | // "Search for messages in "
19 |
20 | class IntentHandler: INExtension, INSearchForPhotosIntentHandling {
21 |
22 | var dataProvider: ListDisplayableDataProvider = MoviesManager()
23 |
24 | override func handler(for intent: INIntent) -> Any {
25 | return self
26 | }
27 |
28 | func handle(searchForPhotos intent: INSearchForPhotosIntent, completion: @escaping (INSearchForPhotosIntentResponse) -> Void) {
29 | guard let searchTerm = intent.searchTerms?.first else {
30 | let response = INSearchForPhotosIntentResponse(code: .failure, userActivity: nil)
31 | completion(response)
32 | return
33 | }
34 |
35 | dataProvider.searchListItems(searchTerm: searchTerm) { (items) in
36 | let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForPhotosIntent.self))
37 | if let movie = items.first {
38 | userActivity.userInfo = [SiriConstants.ItemTitle.rawValue: movie.listItemTitle, SiriConstants.ItemDescription.rawValue: movie.listItemSubtitle ?? ""]
39 | userActivity.title = "Search Results"
40 | userActivity.isEligibleForHandoff = true
41 | userActivity.becomeCurrent()
42 | }
43 |
44 | let response = INSearchForPhotosIntentResponse(code: .continueInApp, userActivity: userActivity)
45 |
46 | completion(response)
47 | }
48 | }
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - "DADependencyInjection/AppDelegate.swift"
3 | - "DADependencyInjection/ViewController.swift"
4 | - "DADependencyInjection/Libraries/.*"
5 | - "DADependencyInjectionTests/.*"
6 | - "**/*ViewController.swift"
7 |
--------------------------------------------------------------------------------