Skip to content

Commit 1d35ef8

Browse files
committed
Merge branch 'feature/instance'
2 parents 653bfa1 + b006e77 commit 1d35ef8

File tree

8 files changed

+331
-19
lines changed

8 files changed

+331
-19
lines changed

.gitmodules

+4
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
path = Carthage/Checkouts/LlamaKit
33
url = https://github.com/LlamaKit/LlamaKit.git
44
[submodule "Carthage/Checkouts/Assertions"]
5+
path = Carthage/Checkouts/Assertions
56
url = https://github.com/ikesyo/Assertions.git
7+
[submodule "Carthage/Checkouts/OHHTTPStubs"]
8+
path = Carthage/Checkouts/OHHTTPStubs
9+
url = https://github.com/ishkawa/OHHTTPStubs.git

APIKit.xcodeproj/project.pbxproj

+20
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
7F0869A61A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */; };
1616
7F0869A71A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */; };
1717
7F0869A81A979088001AD3E1 /* APIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD171A94D085006863BB /* APIKit.swift */; };
18+
7F1B190B1AA2CA1300C7AFCF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */; };
19+
7F1B190C1AA2CA1300C7AFCF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */; };
1820
7F30A8561A975BD600A8C136 /* RequestBodyBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */; };
1921
7F45FD181A94D085006863BB /* APIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD171A94D085006863BB /* APIKit.swift */; };
2022
7F45FD421A94D1CC006863BB /* LlamaKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FD1C1A94D1B4006863BB /* LlamaKit.framework */; };
@@ -30,10 +32,14 @@
3032
7F45FD711A94DA2B006863BB /* LlamaKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FD1C1A94D1B4006863BB /* LlamaKit.framework */; };
3133
7F45FD721A94DA2B006863BB /* LlamaKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FD1C1A94D1B4006863BB /* LlamaKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3234
7F45FD741A94E832006863BB /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD731A94E832006863BB /* Models.swift */; };
35+
7FAC25A01AA2C03400E92500 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FAC259E1AA2C01100E92500 /* OHHTTPStubs.framework */; };
36+
7FAC25A11AA2C04000E92500 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FAC259C1AA2C00600E92500 /* OHHTTPStubs.framework */; };
37+
7FAC25A21AA2C1D500E92500 /* OHHTTPStubs.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FAC259C1AA2C00600E92500 /* OHHTTPStubs.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3338
7FCBE9DD1A9734880075AFD9 /* RequestBodyBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */; };
3439
7FCBE9DE1A9734880075AFD9 /* RequestBodyBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */; };
3540
7FCBE9E01A9734950075AFD9 /* ResponseBodyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */; };
3641
7FCBE9E11A9734950075AFD9 /* ResponseBodyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */; };
42+
7FD65B141AA306BB008DCA2C /* OHHTTPStubs.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FAC259E1AA2C01100E92500 /* OHHTTPStubs.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3743
7FEC5A191A96FE2600B1D3C0 /* ResponseBodyParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FEC5A181A96FE2600B1D3C0 /* ResponseBodyParserTests.swift */; };
3844
7FEC5A1A1A96FE2600B1D3C0 /* APIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; };
3945
7FEC5A211A96FFD300B1D3C0 /* LlamaKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FD1C1A94D1B4006863BB /* LlamaKit.framework */; };
@@ -77,6 +83,7 @@
7783
files = (
7884
CDB0CCDF1A9B2F6700BADAC5 /* Assertions.framework in CopyFiles */,
7985
7F0869A41A9787E3001AD3E1 /* LlamaKit.framework in CopyFiles */,
86+
7FD65B141AA306BB008DCA2C /* OHHTTPStubs.framework in CopyFiles */,
8087
);
8188
runOnlyForDeploymentPostprocessing = 0;
8289
};
@@ -100,6 +107,7 @@
100107
files = (
101108
CDB0CCDC1A9B2ED600BADAC5 /* Assertions.framework in CopyFiles */,
102109
7FEC5A231A97001D00B1D3C0 /* LlamaKit.framework in CopyFiles */,
110+
7FAC25A21AA2C1D500E92500 /* OHHTTPStubs.framework in CopyFiles */,
103111
);
104112
runOnlyForDeploymentPostprocessing = 0;
105113
};
@@ -108,6 +116,7 @@
108116
/* Begin PBXFileReference section */
109117
7F0869941A978790001AD3E1 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
110118
7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedSerialization.swift; sourceTree = "<group>"; };
119+
7F1B190A1AA2CA1300C7AFCF /* APITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APITests.swift; sourceTree = "<group>"; };
111120
7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilderTests.swift; sourceTree = "<group>"; };
112121
7F45FCDD1A94D02C006863BB /* APIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = APIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
113122
7F45FCE11A94D02C006863BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -125,6 +134,8 @@
125134
7F45FD561A94D9A9006863BB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
126135
7F45FD6A1A94D9F9006863BB /* GitHub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHub.swift; sourceTree = "<group>"; };
127136
7F45FD731A94E832006863BB /* Models.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
137+
7FAC259C1AA2C00600E92500 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OHHTTPStubs.framework; sourceTree = "<group>"; };
138+
7FAC259E1AA2C01100E92500 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OHHTTPStubs.framework; sourceTree = "<group>"; };
128139
7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilder.swift; sourceTree = "<group>"; };
129140
7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseBodyParser.swift; sourceTree = "<group>"; };
130141
7FEC5A141A96FE2600B1D3C0 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -140,6 +151,7 @@
140151
buildActionMask = 2147483647;
141152
files = (
142153
7F08699A1A978790001AD3E1 /* APIKit.framework in Frameworks */,
154+
7FAC25A01AA2C03400E92500 /* OHHTTPStubs.framework in Frameworks */,
143155
CDB0CCDE1A9B2F6100BADAC5 /* Assertions.framework in Frameworks */,
144156
7F0869A31A9787E1001AD3E1 /* LlamaKit.framework in Frameworks */,
145157
);
@@ -175,6 +187,7 @@
175187
buildActionMask = 2147483647;
176188
files = (
177189
7FEC5A1A1A96FE2600B1D3C0 /* APIKit.framework in Frameworks */,
190+
7FAC25A11AA2C04000E92500 /* OHHTTPStubs.framework in Frameworks */,
178191
CDB0CCDB1A9B2ECD00BADAC5 /* Assertions.framework in Frameworks */,
179192
7FEC5A211A96FFD300B1D3C0 /* LlamaKit.framework in Frameworks */,
180193
);
@@ -247,6 +260,7 @@
247260
7F45FD1B1A94D1B4006863BB /* iOS */ = {
248261
isa = PBXGroup;
249262
children = (
263+
7FAC259C1AA2C00600E92500 /* OHHTTPStubs.framework */,
250264
CDB0CCDA1A9B2ECD00BADAC5 /* Assertions.framework */,
251265
7F45FD1C1A94D1B4006863BB /* LlamaKit.framework */,
252266
);
@@ -256,6 +270,7 @@
256270
7F45FD1D1A94D1B4006863BB /* Mac */ = {
257271
isa = PBXGroup;
258272
children = (
273+
7FAC259E1AA2C01100E92500 /* OHHTTPStubs.framework */,
259274
CDB0CCDD1A9B2F6100BADAC5 /* Assertions.framework */,
260275
7F45FD1E1A94D1B4006863BB /* LlamaKit.framework */,
261276
);
@@ -288,6 +303,7 @@
288303
7FEC5A151A96FE2600B1D3C0 /* APIKitTests */ = {
289304
isa = PBXGroup;
290305
children = (
306+
7F1B190A1AA2CA1300C7AFCF /* APITests.swift */,
291307
7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */,
292308
7FEC5A181A96FE2600B1D3C0 /* ResponseBodyParserTests.swift */,
293309
7FEC5A161A96FE2600B1D3C0 /* Supporting Files */,
@@ -510,6 +526,7 @@
510526
isa = PBXSourcesBuildPhase;
511527
buildActionMask = 2147483647;
512528
files = (
529+
7F1B190C1AA2CA1300C7AFCF /* APITests.swift in Sources */,
513530
7F0869A01A9787AF001AD3E1 /* RequestBodyBuilderTests.swift in Sources */,
514531
7F0869A11A9787AF001AD3E1 /* ResponseBodyParserTests.swift in Sources */,
515532
);
@@ -552,6 +569,7 @@
552569
isa = PBXSourcesBuildPhase;
553570
buildActionMask = 2147483647;
554571
files = (
572+
7F1B190B1AA2CA1300C7AFCF /* APITests.swift in Sources */,
555573
7FEC5A191A96FE2600B1D3C0 /* ResponseBodyParserTests.swift in Sources */,
556574
7F30A8561A975BD600A8C136 /* RequestBodyBuilderTests.swift in Sources */,
557575
);
@@ -734,6 +752,7 @@
734752
FRAMEWORK_SEARCH_PATHS = (
735753
"$(inherited)",
736754
"$(PROJECT_DIR)/Carthage/Build/iOS",
755+
"$(PROJECT_DIR)/Carthage/Build/Mac",
737756
);
738757
INFOPLIST_FILE = APIKit/Info.plist;
739758
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -756,6 +775,7 @@
756775
FRAMEWORK_SEARCH_PATHS = (
757776
"$(inherited)",
758777
"$(PROJECT_DIR)/Carthage/Build/iOS",
778+
"$(PROJECT_DIR)/Carthage/Build/Mac",
759779
);
760780
INFOPLIST_FILE = APIKit/Info.plist;
761781
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";

APIKit/APIKit.swift

+112-19
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,94 @@ public enum Method: String {
2626
case CONNECT = "CONNECT"
2727
}
2828

29-
public class API {
30-
// configurations
31-
public class func baseURL() -> NSURL {
32-
return NSURL()
33-
}
29+
private var dataTaskResponseBufferKey = 0
30+
private var dataTaskCompletionHandlerKey = 0
3431

35-
public class func URLSession() -> NSURLSession {
36-
return NSURLSession.sharedSession()
32+
private extension NSURLSessionDataTask {
33+
private var responseBuffer: NSMutableData {
34+
if let responseBuffer = objc_getAssociatedObject(self, &dataTaskResponseBufferKey) as? NSMutableData {
35+
return responseBuffer
36+
} else {
37+
let responseBuffer = NSMutableData()
38+
objc_setAssociatedObject(self, &dataTaskResponseBufferKey, responseBuffer, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
39+
return responseBuffer
40+
}
41+
}
42+
43+
private var completionHandler: ((NSData?, NSURLResponse?, NSError?) -> Void)? {
44+
get {
45+
return (objc_getAssociatedObject(self, &dataTaskCompletionHandlerKey) as? Box<(NSData?, NSURLResponse?, NSError?) -> Void>)?.unbox
46+
}
47+
48+
set {
49+
if let value = newValue {
50+
objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, Box(value), UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
51+
} else {
52+
objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, nil, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
53+
}
54+
}
3755
}
56+
}
57+
58+
// use private, global scope variable until we can use stored class var in Swift 1.2
59+
private var instancePairDictionary = [String: (API, NSURLSession)]()
60+
private let instancePairSemaphore = dispatch_semaphore_create(1)
3861

62+
public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate {
63+
// configurations
64+
public class func baseURL() -> NSURL {
65+
fatalError("API.baseURL() must be overrided in subclasses.")
66+
}
67+
3968
public class func requestBodyBuilder() -> RequestBodyBuilder {
4069
return .JSON(writingOptions: nil)
4170
}
4271

4372
public class func responseBodyParser() -> ResponseBodyParser {
4473
return .JSON(readingOptions: nil)
4574
}
75+
76+
public class func URLSessionConfiguration() -> NSURLSessionConfiguration {
77+
return NSURLSessionConfiguration.defaultSessionConfiguration()
78+
}
79+
80+
public class func URLSessionDelegateQueue() -> NSOperationQueue? {
81+
// nil indicates NSURLSession creates its own serial operation queue.
82+
// see doc of NSURLSession.init(configuration:delegate:delegateQueue:) for more details.
83+
return nil
84+
}
85+
86+
// prevent instantiation
87+
override private init() {
88+
super.init()
89+
}
90+
91+
// create session and instance of API for each subclasses
92+
private final class var instancePair: (API, NSURLSession) {
93+
let className = NSStringFromClass(self)
94+
95+
dispatch_semaphore_wait(instancePairSemaphore, DISPATCH_TIME_FOREVER)
96+
let pair: (API, NSURLSession) = instancePairDictionary[className] ?? {
97+
let instance = (self as NSObject.Type)() as API
98+
let configuration = self.URLSessionConfiguration()
99+
let queue = self.URLSessionDelegateQueue()
100+
let session = NSURLSession(configuration: configuration, delegate: instance, delegateQueue: queue)
101+
let pair = (instance, session)
102+
instancePairDictionary[className] = pair
103+
return pair
104+
}()
105+
dispatch_semaphore_signal(instancePairSemaphore)
106+
107+
return pair
108+
}
109+
110+
public final class var instance: API {
111+
return instancePair.0
112+
}
113+
114+
public final class var URLSession: NSURLSession {
115+
return instancePair.1
116+
}
46117

47118
// build NSURLRequest
48119
public class func URLRequest(method: Method, _ path: String, _ parameters: [String: AnyObject] = [:]) -> NSURLRequest? {
@@ -77,11 +148,13 @@ public class API {
77148

78149
// send request and build response object
79150
public class func sendRequest<T: Request>(request: T, handler: (Result<T.Response, NSError>) -> Void = {r in}) -> NSURLSessionDataTask? {
80-
let session = URLSession()
151+
let session = URLSession
81152
let mainQueue = dispatch_get_main_queue()
82153

83154
if let URLRequest = request.URLRequest {
84-
let task = session.dataTaskWithRequest(URLRequest) { data, URLResponse, connectionError in
155+
let task = session.dataTaskWithRequest(URLRequest)
156+
157+
task.completionHandler = { data, URLResponse, connectionError in
85158
if let error = connectionError {
86159
dispatch_async(mainQueue, { handler(.Failure(Box(error))) })
87160
return
@@ -94,18 +167,24 @@ public class API {
94167
dispatch_async(mainQueue, { handler(.Failure(Box(error))) })
95168
return
96169
}
97-
98-
let mappedResponse: Result<T.Response, NSError> = self.responseBodyParser().parseData(data).flatMap { rawResponse in
99-
if let response = request.responseFromObject(rawResponse) {
100-
return success(response)
101-
} else {
102-
let userInfo = [NSLocalizedDescriptionKey: "failed to create model object from raw object."]
103-
let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo)
104-
return failure(error)
170+
171+
if let data = data {
172+
let mappedResponse: Result<T.Response, NSError> = self.responseBodyParser().parseData(data).flatMap { rawResponse in
173+
if let response = request.responseFromObject(rawResponse) {
174+
return success(response)
175+
} else {
176+
let userInfo = [NSLocalizedDescriptionKey: "failed to create model object from raw object."]
177+
let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo)
178+
return failure(error)
179+
}
180+
105181
}
106-
182+
dispatch_async(mainQueue, { handler(mappedResponse) })
183+
} else {
184+
let userInfo = [NSLocalizedDescriptionKey: "unable to get response body despite NSURLSession raised no error."]
185+
let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo)
186+
dispatch_async(mainQueue, { handler(.Failure(Box(error))) })
107187
}
108-
dispatch_async(mainQueue, { handler(mappedResponse) })
109188
}
110189

111190
task.resume()
@@ -119,4 +198,18 @@ public class API {
119198
return nil
120199
}
121200
}
201+
202+
// MARK: - NSURLSessionTaskDelegate
203+
// TODO: add attributes like NS_REQUIRES_SUPER when it is available in future version of Swift.
204+
public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError connectionError: NSError?) {
205+
if let dataTask = task as? NSURLSessionDataTask {
206+
dataTask.completionHandler?(dataTask.responseBuffer, dataTask.response, connectionError)
207+
}
208+
}
209+
210+
// MARK: - NSURLSessionDataDelegate
211+
// TODO: add attributes like NS_REQUIRES_SUPER when it is available in future version of Swift.
212+
public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
213+
dataTask.responseBuffer.appendData(data)
214+
}
122215
}

0 commit comments

Comments
 (0)