From ad0b537c2db70cc8e841881dd48b6590235e2b75 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Mon, 16 Mar 2015 18:59:01 +0900 Subject: [PATCH 01/10] Add defaultURLSession and delegate for it instead of instancePair --- APIKit/APIKit.swift | 68 ++++++++++---------------------------- APIKitTests/APITests.swift | 15 --------- 2 files changed, 18 insertions(+), 65 deletions(-) diff --git a/APIKit/APIKit.swift b/APIKit/APIKit.swift index a229e7bb..2df95332 100644 --- a/APIKit/APIKit.swift +++ b/APIKit/APIKit.swift @@ -56,10 +56,13 @@ private extension NSURLSessionDataTask { } // use private, global scope variable until we can use stored class var in Swift 1.2 -private var instancePairDictionary = [String: (API, NSURLSession)]() -private let instancePairSemaphore = dispatch_semaphore_create(1) +private let internalDefaultURLSession = NSURLSession( + configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), + delegate: URLSessionDelegate(), + delegateQueue: nil +) -public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { +public class API { // configurations public class func baseURL() -> NSURL { fatalError("API.baseURL() must be overrided in subclasses.") @@ -72,47 +75,9 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { public class func responseBodyParser() -> ResponseBodyParser { return .JSON(readingOptions: nil) } - - public class func URLSessionConfiguration() -> NSURLSessionConfiguration { - return NSURLSessionConfiguration.defaultSessionConfiguration() - } - - public class func URLSessionDelegateQueue() -> NSOperationQueue? { - // nil indicates NSURLSession creates its own serial operation queue. - // see doc of NSURLSession.init(configuration:delegate:delegateQueue:) for more details. - return nil - } - - // prevent instantiation - override private init() { - super.init() - } - - // create session and instance of API for each subclasses - private final class var instancePair: (API, NSURLSession) { - let className = NSStringFromClass(self) - - dispatch_semaphore_wait(instancePairSemaphore, DISPATCH_TIME_FOREVER) - let pair: (API, NSURLSession) = instancePairDictionary[className] ?? { - let instance = (self as NSObject.Type)() as API - let configuration = self.URLSessionConfiguration() - let queue = self.URLSessionDelegateQueue() - let session = NSURLSession(configuration: configuration, delegate: instance, delegateQueue: queue) - let pair = (instance, session) - instancePairDictionary[className] = pair - return pair - }() - dispatch_semaphore_signal(instancePairSemaphore) - - return pair - } - - public final class var instance: API { - return instancePair.0 - } - - public final class var URLSession: NSURLSession { - return instancePair.1 + + public class var defaultURLSession: NSURLSession { + return internalDefaultURLSession } // build NSURLRequest @@ -148,11 +113,14 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { // send request and build response object public class func sendRequest(request: T, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { - let session = URLSession + return sendRequest(request, URLSession: defaultURLSession, handler: handler) + } + + public class func sendRequest(request: T, URLSession: NSURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { let mainQueue = dispatch_get_main_queue() if let URLRequest = request.URLRequest { - let task = session.dataTaskWithRequest(URLRequest) + let task = URLSession.dataTaskWithRequest(URLRequest) task.completionHandler = { data, URLResponse, connectionError in if let error = connectionError { @@ -192,9 +160,10 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { return nil } } - +} + +public class URLSessionDelegate: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { // MARK: - NSURLSessionTaskDelegate - // TODO: add attributes like NS_REQUIRES_SUPER when it is available in future version of Swift. public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError connectionError: NSError?) { if let dataTask = task as? NSURLSessionDataTask { dataTask.completionHandler?(dataTask.responseBuffer, dataTask.response, connectionError) @@ -202,8 +171,7 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { } // MARK: - NSURLSessionDataDelegate - // TODO: add attributes like NS_REQUIRES_SUPER when it is available in future version of Swift. public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) { dataTask.responseBuffer.appendData(data) - } + } } diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 89c56b17..3309779c 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -41,21 +41,6 @@ class APITests: XCTestCase { super.tearDown() } - // MARK: - instance tests - func testDifferentSessionsAreCreatedForEachClasses() { - assert(MockAPI.URLSession, !=, AnotherMockAPI.URLSession) - } - - func testSameSessionsAreUsedInSameClasses() { - assertEqual(MockAPI.URLSession, MockAPI.URLSession) - assertEqual(AnotherMockAPI.URLSession, AnotherMockAPI.URLSession) - } - - func testDelegateOfSessions() { - assertNotNil(MockAPI.URLSession.delegate as? MockAPI) - assertNotNil(AnotherMockAPI.URLSession.delegate as? AnotherMockAPI) - } - // MARK: - integration tests func testSuccess() { let dictionary = ["key": "value"] From e08ab00faf3ac8c11c0aded20693a9a8802eff59 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 17 Mar 2015 13:01:57 +0900 Subject: [PATCH 02/10] add acceptableStatusCodeRange class var to API --- APIKit/APIKit.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/APIKit/APIKit.swift b/APIKit/APIKit.swift index a229e7bb..5ddba068 100644 --- a/APIKit/APIKit.swift +++ b/APIKit/APIKit.swift @@ -115,6 +115,10 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { return instancePair.1 } + public class var acceptableStatusCodeRange: Range { + return 200..<300 + } + // build NSURLRequest public class func URLRequest(method: Method, _ path: String, _ parameters: [String: AnyObject] = [:]) -> NSURLRequest? { if let components = NSURLComponents(URL: baseURL(), resolvingAgainstBaseURL: true) { @@ -161,7 +165,7 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { } let statusCode = (URLResponse as? NSHTTPURLResponse)?.statusCode ?? 0 - if !contains(200..<300, statusCode) { + if !contains(self.acceptableStatusCodeRange, statusCode) { let userInfo = [NSLocalizedDescriptionKey: "received status code that represents error"] let error = NSError(domain: APIKitErrorDomain, code: statusCode, userInfo: userInfo) dispatch_async(mainQueue, { handler(.Failure(Box(error))) }) From fb7bff12b913475e9628bd713b32bbfc78f88230 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 17 Mar 2015 13:18:24 +0900 Subject: [PATCH 03/10] Add responseErrorFromObject method to give a chance to create more detailed error using response --- APIKit/APIKit.swift | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/APIKit/APIKit.swift b/APIKit/APIKit.swift index 5ddba068..43f9ddb6 100644 --- a/APIKit/APIKit.swift +++ b/APIKit/APIKit.swift @@ -166,9 +166,14 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { let statusCode = (URLResponse as? NSHTTPURLResponse)?.statusCode ?? 0 if !contains(self.acceptableStatusCodeRange, statusCode) { - let userInfo = [NSLocalizedDescriptionKey: "received status code that represents error"] - let error = NSError(domain: APIKitErrorDomain, code: statusCode, userInfo: userInfo) - dispatch_async(mainQueue, { handler(.Failure(Box(error))) }) + var error: NSError = { + switch self.responseBodyParser().parseData(data) { + case .Success(let box): return self.responseErrorFromObject(box.unbox) + case .Failure(let box): return box.unbox + } + }() + + dispatch_async(mainQueue) { handler(failure(error)) } return } @@ -180,8 +185,8 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) return failure(error) } - } + dispatch_async(mainQueue, { handler(mappedResponse) }) } @@ -196,7 +201,13 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { return nil } } - + + public class func responseErrorFromObject(object: AnyObject) -> NSError { + let userInfo = [NSLocalizedDescriptionKey: "received status code that represents error"] + let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) + return error + } + // MARK: - NSURLSessionTaskDelegate // TODO: add attributes like NS_REQUIRES_SUPER when it is available in future version of Swift. public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError connectionError: NSError?) { From 3dbb32545605660fe3d158be2db719bbbf98a17a Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 17 Mar 2015 13:24:45 +0900 Subject: [PATCH 04/10] fix tests --- APIKitTests/APITests.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 89c56b17..5f7b89d8 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -17,6 +17,10 @@ class APITests: XCTestCase { override class func responseBodyParser() -> ResponseBodyParser { return .JSON(readingOptions: nil) } + + override class func responseErrorFromObject(object: AnyObject) -> NSError { + return NSError(domain: "MockAPIErrorDomain", code: 10000, userInfo: nil) + } class Endpoint { class Get: Request { @@ -118,7 +122,8 @@ class APITests: XCTestCase { OHHTTPStubs.stubRequestsPassingTest({ request in return true }, withStubResponse: { request in - return OHHTTPStubsResponse(data: NSData(), statusCode: 400, headers: nil) + let data = NSJSONSerialization.dataWithJSONObject([:], options: nil, error: nil)! + return OHHTTPStubsResponse(data: data, statusCode: 400, headers: nil) }) let expectation = expectationWithDescription("wait for response") @@ -131,8 +136,8 @@ class APITests: XCTestCase { case .Failure(let box): let error = box.unbox - assertEqual(error.domain, APIKitErrorDomain) - assertEqual(error.code, 400) + assertEqual(error.domain, "MockAPIErrorDomain") + assertEqual(error.code, 10000) } expectation.fulfill() From 08e237ca2912fe048691f666eee524ec14ee7aaf Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 17 Mar 2015 15:05:45 +0900 Subject: [PATCH 05/10] Update README --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 85da3d19..ebd05295 100644 --- a/README.md +++ b/README.md @@ -169,19 +169,21 @@ class GitHub: API { ## Advanced usage - ### NSURLSessionDelegate -APIKit creates singleton instances for each subclasses of API and set them as delegates of NSURLSession, -so you can add following features by implementing delegate methods. +You can add custom behavior of `NSURLSession` by following steps: + +1. Create subclass of `URLSessionDelegate` (e.g. `MyAPIURLSessionDelegate`). +2. Implement additional delegate methods in it. +3. Override `defaultURLSession` of `API` and return `NSURLSession` that has `MyURLSessionDelegate` as a delegate. + +This can add following features: - Hook events of NSURLSession - Handle authentication challenges - Convert a data task to NSURLSessionDownloadTask -#### Overriding delegate methods implemented by API - -API class also uses delegate methods of NSURLSession to implement wrapper of NSURLSession, so you should call super if you override following methods. +NOTE: `URLSessionDelegate` also implements delegate methods of `NSURLSession` to implement wrapper of `NSURLSession`, so you should call super if you override following methods. - `func URLSession(session:task:didCompleteWithError:)` - `func URLSession(session:dataTask:didReceiveData:)` From ff11930590d90793ee8bab50b77b24f544ce3a3d Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 17 Mar 2015 23:55:27 +0900 Subject: [PATCH 06/10] use let for immutable variable --- APIKit/APIKit.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/APIKit/APIKit.swift b/APIKit/APIKit.swift index 43f9ddb6..716a2675 100644 --- a/APIKit/APIKit.swift +++ b/APIKit/APIKit.swift @@ -166,7 +166,7 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { let statusCode = (URLResponse as? NSHTTPURLResponse)?.statusCode ?? 0 if !contains(self.acceptableStatusCodeRange, statusCode) { - var error: NSError = { + let error: NSError = { switch self.responseBodyParser().parseData(data) { case .Success(let box): return self.responseErrorFromObject(box.unbox) case .Failure(let box): return box.unbox From a83fd915fc520ab465f07ee07210ed309062272a Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 17 Mar 2015 23:57:41 +0900 Subject: [PATCH 07/10] use [Int] for more flexible status codes --- APIKit/APIKit.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/APIKit/APIKit.swift b/APIKit/APIKit.swift index 716a2675..9ce05c74 100644 --- a/APIKit/APIKit.swift +++ b/APIKit/APIKit.swift @@ -115,8 +115,8 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { return instancePair.1 } - public class var acceptableStatusCodeRange: Range { - return 200..<300 + public class var acceptableStatusCodes: [Int] { + return [Int](200..<300) } // build NSURLRequest @@ -165,7 +165,7 @@ public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { } let statusCode = (URLResponse as? NSHTTPURLResponse)?.statusCode ?? 0 - if !contains(self.acceptableStatusCodeRange, statusCode) { + if !contains(self.acceptableStatusCodes, statusCode) { let error: NSError = { switch self.responseBodyParser().parseData(data) { case .Success(let box): return self.responseErrorFromObject(box.unbox) From 5e852d148c4ab127bcb8f1a2a60bf01ef9f2c754 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Wed, 18 Mar 2015 00:33:47 +0900 Subject: [PATCH 08/10] add comments about convenience func --- APIKit/APIKit.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/APIKit/APIKit.swift b/APIKit/APIKit.swift index 2df95332..8a73da5f 100644 --- a/APIKit/APIKit.swift +++ b/APIKit/APIKit.swift @@ -111,12 +111,13 @@ public class API { } } - // send request and build response object + // this methods can be removed in Swift 1.2 public class func sendRequest(request: T, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { return sendRequest(request, URLSession: defaultURLSession, handler: handler) } - public class func sendRequest(request: T, URLSession: NSURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { + // send request and build response object + public class func sendRequest(request: T, URLSession: NSURLSession = defaultURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { let mainQueue = dispatch_get_main_queue() if let URLRequest = request.URLRequest { From 87a02f2fffc5471cabfe8590b77baf87fa23725d Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Wed, 18 Mar 2015 10:40:46 +0900 Subject: [PATCH 09/10] remove default argument of in --- APIKit/APIKit.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/APIKit/APIKit.swift b/APIKit/APIKit.swift index 8a73da5f..33e322e2 100644 --- a/APIKit/APIKit.swift +++ b/APIKit/APIKit.swift @@ -111,13 +111,18 @@ public class API { } } - // this methods can be removed in Swift 1.2 + // In Swift 1.1, we could not omit `URLSession` argument of `func send(request:URLSession(=default):handler(=default):)` + // with trailing closure, so we provide following 2 methods + // - `func sendRequest(request:handler(=default):)` + // - `func sendRequest(request:URLSession:handler(=default):)`. + // In Swift 1.2, we can omit default arguments with trailing closure, so they should be replaced with + // - `func sendRequest(request:URLSession(=default):handler(=default):)` public class func sendRequest(request: T, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { return sendRequest(request, URLSession: defaultURLSession, handler: handler) } // send request and build response object - public class func sendRequest(request: T, URLSession: NSURLSession = defaultURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { + public class func sendRequest(request: T, URLSession: NSURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { let mainQueue = dispatch_get_main_queue() if let URLRequest = request.URLRequest { From cd25e0972205ff81149e5616e2aca40b30f7dec8 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Wed, 18 Mar 2015 10:42:39 +0900 Subject: [PATCH 10/10] Fix Engligh in README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ebd05295..7580cf78 100644 --- a/README.md +++ b/README.md @@ -171,11 +171,11 @@ class GitHub: API { ### NSURLSessionDelegate -You can add custom behavior of `NSURLSession` by following steps: +You can add custom behaviors of `NSURLSession` by following steps: -1. Create subclass of `URLSessionDelegate` (e.g. `MyAPIURLSessionDelegate`). +1. Create a subclass of `URLSessionDelegate` (e.g. `MyAPIURLSessionDelegate`). 2. Implement additional delegate methods in it. -3. Override `defaultURLSession` of `API` and return `NSURLSession` that has `MyURLSessionDelegate` as a delegate. +3. Override `defaultURLSession` of `API` and return `NSURLSession` that has `MyURLSessionDelegate` as its delegate. This can add following features: