Compare commits

...

1 Commits

Author SHA1 Message Date
Olivier Halligon 219e7fd5c1 Added SWAPIProviders that implement the swapi.co WS 2015-10-11 00:44:44 +02:00
11 changed files with 350 additions and 23 deletions
+24
View File
@@ -27,6 +27,12 @@
09D796071BC73E8B003C68EB /* StoryboardConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D796061BC73E8B003C68EB /* StoryboardConstants.swift */; settings = {ASSET_TAGS = (); }; };
09D7960D1BC7431C003C68EB /* FetchableTrait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D7960C1BC7431C003C68EB /* FetchableTrait.swift */; settings = {ASSET_TAGS = (); }; };
09D796111BC97809003C68EB /* mainPilot.plist in Resources */ = {isa = PBXBuildFile; fileRef = 09D796101BC97809003C68EB /* mainPilot.plist */; settings = {ASSET_TAGS = (); }; };
09D796131BC9A5BC003C68EB /* SWAPIPersonProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D796121BC9A5BC003C68EB /* SWAPIPersonProvider.swift */; settings = {ASSET_TAGS = (); }; };
09D796151BC9A5FC003C68EB /* NetworkLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D796141BC9A5FC003C68EB /* NetworkLayer.swift */; settings = {ASSET_TAGS = (); }; };
09D796171BC9B53D003C68EB /* URLSessionNetworkLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D796161BC9B53D003C68EB /* URLSessionNetworkLayer.swift */; settings = {ASSET_TAGS = (); }; };
09D796191BC9BA49003C68EB /* DependencyContainers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D796181BC9BA49003C68EB /* DependencyContainers.swift */; settings = {ASSET_TAGS = (); }; };
09D7961B1BC9BE65003C68EB /* SWAPIStarshipProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D7961A1BC9BE65003C68EB /* SWAPIStarshipProvider.swift */; settings = {ASSET_TAGS = (); }; };
09D7961D1BC9C62E003C68EB /* SWAPICommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D7961C1BC9C62E003C68EB /* SWAPICommon.swift */; settings = {ASSET_TAGS = (); }; };
607FACEC1AFB9204008FA782 /* SWAPIWebServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* SWAPIWebServiceTests.swift */; };
7BBD849465D99D9D1987AE6D /* Pods_DipTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 304AD039660A2C58EB08D985 /* Pods_DipTests.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
84D8EBE5B2D583BEFB17C45A /* Pods_DipSampleApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FE9C70E965FF88C3F20AC76 /* Pods_DipSampleApp.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
@@ -56,6 +62,12 @@
09D796061BC73E8B003C68EB /* StoryboardConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardConstants.swift; sourceTree = "<group>"; };
09D7960C1BC7431C003C68EB /* FetchableTrait.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableTrait.swift; sourceTree = "<group>"; };
09D796101BC97809003C68EB /* mainPilot.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = mainPilot.plist; sourceTree = "<group>"; };
09D796121BC9A5BC003C68EB /* SWAPIPersonProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SWAPIPersonProvider.swift; sourceTree = "<group>"; };
09D796141BC9A5FC003C68EB /* NetworkLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkLayer.swift; sourceTree = "<group>"; };
09D796161BC9B53D003C68EB /* URLSessionNetworkLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionNetworkLayer.swift; sourceTree = "<group>"; };
09D796181BC9BA49003C68EB /* DependencyContainers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DependencyContainers.swift; sourceTree = "<group>"; };
09D7961A1BC9BE65003C68EB /* SWAPIStarshipProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SWAPIStarshipProvider.swift; sourceTree = "<group>"; };
09D7961C1BC9C62E003C68EB /* SWAPICommon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SWAPICommon.swift; sourceTree = "<group>"; };
2FE9C70E965FF88C3F20AC76 /* Pods_DipSampleApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DipSampleApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
304AD039660A2C58EB08D985 /* Pods_DipTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DipTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
607FACE51AFB9204008FA782 /* DipTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DipTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -103,9 +115,11 @@
0900121A1BC6FECA0079C600 /* Providers */ = {
isa = PBXGroup;
children = (
09D7961C1BC9C62E003C68EB /* SWAPICommon.swift */,
090012371BC6FEEA0079C600 /* APIs */,
090012381BC6FEFD0079C600 /* PersonProviders */,
090012391BC6FF080079C600 /* StarshipProviders */,
09D796161BC9B53D003C68EB /* URLSessionNetworkLayer.swift */,
);
path = Providers;
sourceTree = "<group>";
@@ -125,6 +139,7 @@
children = (
0900121B1BC6FECA0079C600 /* PersonProviderAPI.swift */,
0900123C1BC7012A0079C600 /* StarshipProviderAPI.swift */,
09D796141BC9A5FC003C68EB /* NetworkLayer.swift */,
);
name = APIs;
sourceTree = "<group>";
@@ -134,6 +149,7 @@
children = (
0900121C1BC6FECA0079C600 /* DummyPilotProvider.swift */,
0900121E1BC6FECA0079C600 /* PlistPersonProvider.swift */,
09D796121BC9A5BC003C68EB /* SWAPIPersonProvider.swift */,
);
name = PersonProviders;
sourceTree = "<group>";
@@ -143,6 +159,7 @@
children = (
090012431BC708A00079C600 /* DummyStarshipProvider.swift */,
0900121D1BC6FECA0079C600 /* HardCodedStarshipProvider.swift */,
09D7961A1BC9BE65003C68EB /* SWAPIStarshipProvider.swift */,
);
name = StarshipProviders;
sourceTree = "<group>";
@@ -161,6 +178,7 @@
children = (
0900123A1BC6FF4D0079C600 /* Main.storyboard */,
099022611BC123C000E76F43 /* AppDelegate.swift */,
09D796181BC9BA49003C68EB /* DependencyContainers.swift */,
0900123E1BC704A80079C600 /* Model */,
0900121A1BC6FECA0079C600 /* Providers */,
090012201BC6FECA0079C600 /* ViewControllers */,
@@ -442,15 +460,19 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
09D796151BC9A5FC003C68EB /* NetworkLayer.swift in Sources */,
09D795FF1BC71F5A003C68EB /* PersonListViewController.swift in Sources */,
0900122A1BC6FECA0079C600 /* PersonCell.swift in Sources */,
09D796011BC722C0003C68EB /* StarshipListViewController.swift in Sources */,
0900122C1BC6FECA0079C600 /* PersonProviderAPI.swift in Sources */,
09D7961D1BC9C62E003C68EB /* SWAPICommon.swift in Sources */,
090012291BC6FECA0079C600 /* BaseCell.swift in Sources */,
0900122D1BC6FECA0079C600 /* DummyPilotProvider.swift in Sources */,
099022621BC123C000E76F43 /* AppDelegate.swift in Sources */,
09D796131BC9A5BC003C68EB /* SWAPIPersonProvider.swift in Sources */,
090012421BC7059E0079C600 /* Starship.swift in Sources */,
0900123D1BC7012A0079C600 /* StarshipProviderAPI.swift in Sources */,
09D7961B1BC9BE65003C68EB /* SWAPIStarshipProvider.swift in Sources */,
0900122E1BC6FECA0079C600 /* HardCodedStarshipProvider.swift in Sources */,
09D796071BC73E8B003C68EB /* StoryboardConstants.swift in Sources */,
09D796031BC72691003C68EB /* StarshipCell.swift in Sources */,
@@ -458,6 +480,8 @@
090012441BC708A00079C600 /* DummyStarshipProvider.swift in Sources */,
09D7960D1BC7431C003C68EB /* FetchableTrait.swift in Sources */,
0900122F1BC6FECA0079C600 /* PlistPersonProvider.swift in Sources */,
09D796191BC9BA49003C68EB /* DependencyContainers.swift in Sources */,
09D796171BC9B53D003C68EB /* URLSessionNetworkLayer.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+1 -19
View File
@@ -1,30 +1,12 @@
//
// AppDelegate.swift
// DipSampleApp
// Dip
//
// Created by Olivier Halligon on 04/10/2015.
// Copyright © 2015 AliSoftware. All rights reserved.
//
import UIKit
import Dip
let dip: DependencyContainer<Int> = {
let dip = DependencyContainer<Int>()
// 1) Register the PersonProviderAPI singleton, one generic and one specific for a specific personID
dip.register(instance: DummyPilotProvider() as PersonProviderAPI)
let mainPersonProvider = PlistPersonProvider(plist: "mainPilot")
dip.register(0, instance: mainPersonProvider as PersonProviderAPI)
// 2) Register the StarshipProviderAPI factories, one generic and one specific for a specific starshipID
dip.register() { HardCodedStarshipProvider() as StarshipProviderAPI }
let pilotName = mainPersonProvider.people[0].name
dip.register(0) { DummyStarshipProvider(pilotName: pilotName) as StarshipProviderAPI }
return dip
}()
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -0,0 +1,66 @@
//
// DependencyContainers.swift
// Dip
//
// Created by Olivier Halligon on 10/10/2015.
// Copyright © 2015 AliSoftware. All rights reserved.
//
import Foundation
import Dip
enum WebService {
case PersonWS
case StarshipWS
}
// Dependency Container for WebServices & NetworkLayer
let wsDependencies: DependencyContainer<WebService> = {
let dip = DependencyContainer<WebService>()
// Register the NetworkLayer, same for everyone here (but we have the ability to register a different one for a specific WebService if we wanted to)
dip.register(instance: URLSessionNetworkLayer(baseURL: "http://swapi.co/api/")! as NetworkLayer)
return dip
}()
/* Change this to toggle between real and fake data */
let FAKE_PERSONS = false
let FAKE_STARSHIPS = false
// Dependency Container for Providers
let providerDependencies: DependencyContainer<Int> = {
let dip = DependencyContainer<Int>()
if FAKE_PERSONS {
// 1) Register the PersonProviderAPI singleton, one generic and one specific for a specific personID
dip.register(instance: DummyPilotProvider() as PersonProviderAPI)
dip.register(0, instance: PlistPersonProvider(plist: "mainPilot") as PersonProviderAPI)
} else {
// 1) Register the SWAPIPersonProvider (that hits the real swapi.co WebService)
dip.register(instance: SWAPIPersonProvider() as PersonProviderAPI)
}
if FAKE_STARSHIPS {
// 2) Register the StarshipProviderAPI factories, one generic and one specific for a specific starshipID
dip.register() { HardCodedStarshipProvider() as StarshipProviderAPI }
dip.register(0) { DummyStarshipProvider(pilotName: "Main Pilot") as StarshipProviderAPI }
} else {
// 2) Register the SWAPIStarshipProvider (that hits the real swapi.co WebService)
dip.register(instance: SWAPIStarshipProvider() as StarshipProviderAPI)
}
return dip
}()
@@ -0,0 +1,32 @@
//
// NetworkLayer.swift
// Dip
//
// Created by Olivier Halligon on 10/10/2015.
// Copyright © 2015 AliSoftware. All rights reserved.
//
import Foundation
enum NetworkResponse {
case Success(NSData, NSHTTPURLResponse)
case Error(NSError)
func unwrap() throws -> (NSData, NSHTTPURLResponse) {
switch self {
case Success(let data, let response):
return (data, response)
case Error(let error):
throw error
}
}
func json() throws -> AnyObject {
let (data, _) = try self.unwrap()
return try NSJSONSerialization.JSONObjectWithData(data, options: [])
}
}
protocol NetworkLayer {
func request(path: String, completion: NetworkResponse -> Void)
}
@@ -0,0 +1,19 @@
//
// SWAPICommon.swift
// Dip
//
// Created by Olivier Halligon on 11/10/2015.
// Copyright © 2015 AliSoftware. All rights reserved.
//
import Foundation
enum SWAPIError: ErrorType {
case InvalidJSON
}
func idFromURLString(urlString: String) -> Int? {
let url = NSURL(string: urlString)
let idString = url.flatMap { $0.lastPathComponent }
return idString.flatMap { Int($0) }
}
@@ -0,0 +1,64 @@
//
// SWAPIPersonProvider.swift
// Dip
//
// Created by Olivier Halligon on 10/10/2015.
// Copyright © 2015 AliSoftware. All rights reserved.
//
import Foundation
struct SWAPIPersonProvider : PersonProviderAPI {
let ws = wsDependencies.resolve(.PersonWS) as NetworkLayer
func fetchIDs(completion: [Int] -> Void) {
ws.request("people") { response in
do {
let dict = try response.json()
guard let results = dict["results"] as? [NSDictionary] else { throw SWAPIError.InvalidJSON }
// Extract URLs (flatten to ignore invalid ones)
let urlStrings = results.flatMap({ $0["url"] as? String })
let ids = urlStrings.flatMap(idFromURLString)
completion(ids)
}
catch {
completion([])
}
}
}
func fetch(id: Int, completion: Person? -> Void) {
ws.request("people/\(id)") { response in
do {
let json = try response.json()
guard let dict = json as? NSDictionary,
let name = dict["name"] as? String,
let heightStr = dict["height"] as? String, height = Int(heightStr),
let massStr = dict["mass"] as? String, mass = Int(massStr),
let hairColor = dict["hair_color"] as? String,
let eyeColor = dict["eye_color"] as? String,
let gender = dict["gender"] as? String,
let starshipURLStrings = dict["starships"] as? [String]
else {
throw SWAPIError.InvalidJSON
}
let person = Person(
name: name,
height: height,
mass: mass,
hairColor: hairColor,
eyeColor: eyeColor,
gender: Gender(rawValue: gender),
starshipIDs: starshipURLStrings.flatMap(idFromURLString)
)
completion(person)
}
catch {
completion(nil)
}
}
}
}
@@ -0,0 +1,62 @@
//
// SWAPIStarshipProvider.swift
// Dip
//
// Created by Olivier Halligon on 10/10/2015.
// Copyright © 2015 AliSoftware. All rights reserved.
//
import Foundation
struct SWAPIStarshipProvider : StarshipProviderAPI {
let ws = wsDependencies.resolve(.StarshipWS) as NetworkLayer
func fetchIDs(completion: [Int] -> Void) {
ws.request("starships") { response in
do {
let dict = try response.json()
guard let results = dict["results"] as? [NSDictionary] else { throw SWAPIError.InvalidJSON }
// Extract URLs (flatten to ignore invalid ones)
let urlStrings = results.flatMap({ $0["url"] as? String })
let ids = urlStrings.flatMap(idFromURLString)
completion(ids)
}
catch {
completion([])
}
}
}
func fetch(id: Int, completion: Starship? -> Void) {
ws.request("starships/\(id)") { response in
do {
let json = try response.json()
guard let dict = json as? NSDictionary,
let name = dict["name"] as? String,
let model = dict["model"] as? String,
let manufacturer = dict["manufacturer"] as? String,
let crewStr = dict["crew"] as? String, crew = Int(crewStr),
let passengersStr = dict["passengers"] as? String, passengers = Int(passengersStr),
let pilotIDStrings = dict["pilots"] as? [String]
else {
throw SWAPIError.InvalidJSON
}
let ship = Starship(
name: name,
model: model,
manufacturer: manufacturer,
crew: crew,
passengers: passengers,
pilotIDs: pilotIDStrings.flatMap(idFromURLString)
)
completion(ship)
}
catch {
completion(nil)
}
}
}
}
@@ -0,0 +1,44 @@
//
// URLSessionNetworkLayer.swift
// Dip
//
// Created by Olivier Halligon on 10/10/2015.
// Copyright © 2015 AliSoftware. All rights reserved.
//
import Foundation
struct URLSessionNetworkLayer : NetworkLayer {
let baseURL: NSURL
let session: NSURLSession
let responseQueue: dispatch_queue_t
init?(baseURL: String, session: NSURLSession = .sharedSession(), responseQueue: dispatch_queue_t = dispatch_get_main_queue()) {
guard let url = NSURL(string: baseURL) else { return nil }
self.init(baseURL: url, session: session)
}
init(baseURL: NSURL, session: NSURLSession = .sharedSession(), responseQueue: dispatch_queue_t = dispatch_get_main_queue()) {
self.baseURL = baseURL
self.session = session
self.responseQueue = responseQueue
}
func request(path: String, completion: NetworkResponse -> Void) {
let url = self.baseURL.URLByAppendingPathComponent(path)
let task = session.dataTaskWithURL(url) { data, response, error in
if let data = data, let response = response as? NSHTTPURLResponse {
dispatch_async(self.responseQueue) {
completion(NetworkResponse.Success(data, response))
}
}
else {
let err = error ?? NSError(domain: NSURLErrorDomain, code: NSURLError.Unknown.rawValue, userInfo: nil)
dispatch_async(self.responseQueue) {
completion(NetworkResponse.Error(err))
}
}
}
task.resume()
}
}
@@ -16,6 +16,7 @@ protocol FetchableTrait: class {
var fetchIDs: ([Int] -> Void) -> Void { get }
var fetchOne: (Int, ObjectType? -> Void) -> Void { get }
var fetchProgress: (current: Int, total: Int?) { get set }
}
extension FetchableTrait {
@@ -24,6 +25,7 @@ extension FetchableTrait {
let batch = self.batchRequestID
objects?.removeAll()
fetchProgress = (0,objectIDs.count)
for objectID in objectIDs {
fetchOne(objectID) { (object: ObjectType?) in
// Exit if we failed to retrive an object for this ID, or if the request
@@ -32,6 +34,7 @@ extension FetchableTrait {
if self.objects == nil { self.objects = [] }
self.objects?.append(object)
self.fetchProgress = (self.objects?.count ?? 0, objectIDs.count)
self.tableView?.reloadData()
}
}
@@ -40,10 +43,29 @@ extension FetchableTrait {
func fetchAllObjects() {
self.batchRequestID += 1
let batch = self.batchRequestID
fetchProgress = (0, nil)
fetchIDs() { objectIDs in
guard batch == self.batchRequestID else { return }
self.fetchObjects(objectIDs)
}
}
func displayProgressInNavBar(navigationItem: UINavigationItem) {
let text: String
if let total = fetchProgress.total {
if fetchProgress.current == fetchProgress.total {
text = "Done."
} else {
text = "Loading \(fetchProgress.current) / \(total)"
}
} else {
text = "Loading IDs…"
}
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
label.text = text
label.textColor = .grayColor()
label.font = .systemFontOfSize(12)
label.sizeToFit()
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: label)
}
}
@@ -12,11 +12,17 @@ class PersonListViewController: UITableViewController, FetchableTrait {
var objects: [Person]?
var batchRequestID = 0
lazy var fetchIDs: ([Int] -> Void) -> Void = (dip.resolve() as PersonProviderAPI).fetchIDs
lazy var fetchIDs: ([Int] -> Void) -> Void = (providerDependencies.resolve() as PersonProviderAPI).fetchIDs
lazy var fetchOne: (Int, Person? -> Void) -> Void = { personID, completion in
let provider = dip.resolve(personID) as PersonProviderAPI
let provider = providerDependencies.resolve(personID) as PersonProviderAPI
return provider.fetch(personID, completion: completion)
}
var fetchProgress: (current: Int, total: Int?) = (0, nil) {
didSet {
displayProgressInNavBar(self.navigationItem)
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
guard
@@ -12,13 +12,19 @@ class StarshipListViewController : UITableViewController, FetchableTrait {
var objects: [Starship]?
var batchRequestID = 0
var provider: (Int? -> StarshipProviderAPI) = { dip.resolve($0) }
var provider: (Int? -> StarshipProviderAPI) = { providerDependencies.resolve($0) }
lazy var fetchIDs: ([Int] -> Void) -> Void = self.provider(nil).fetchIDs
lazy var fetchOne: (Int, Starship? -> Void) -> Void = { shipID, completion in
self.provider(shipID).fetch(shipID, completion: completion)
}
var fetchProgress: (current: Int, total: Int?) = (0, nil) {
didSet {
displayProgressInNavBar(self.navigationItem)
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
guard
let id = segue.identifier, segueID = UIStoryboard.Segue.Main(rawValue: id)