Skip to content

Commit

Permalink
Implement JSON network response
Browse files Browse the repository at this point in the history
  • Loading branch information
alasdairlaw committed Dec 19, 2016
1 parent 1683d92 commit 30a49e4
Show file tree
Hide file tree
Showing 8 changed files with 404 additions and 12 deletions.
28 changes: 28 additions & 0 deletions NetworkOperation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@
objects = {

/* Begin PBXBuildFile section */
816D2CF91E07309200300E28 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 816D2CF81E07309200300E28 /* MockURLSession.swift */; };
816D2CFD1E07572500300E28 /* NetworkResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 816D2CFC1E07572500300E28 /* NetworkResponse.swift */; };
816D2CFF1E07573000300E28 /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 816D2CFE1E07573000300E28 /* JSONResponse.swift */; };
816D2D011E0760B100300E28 /* MockSessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 816D2D001E0760B100300E28 /* MockSessionDataTask.swift */; };
818A205F1E05EECF00B835F6 /* NetworkOperation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 818A20551E05EECF00B835F6 /* NetworkOperation.framework */; };
818A20641E05EECF00B835F6 /* NetworkOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818A20631E05EECF00B835F6 /* NetworkOperationTests.swift */; };
818A20661E05EECF00B835F6 /* NetworkOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 818A20581E05EECF00B835F6 /* NetworkOperation.h */; settings = {ATTRIBUTES = (Public, ); }; };
818A20701E05EF5F00B835F6 /* NetworkOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818A206F1E05EF5F00B835F6 /* NetworkOperation.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand All @@ -23,12 +28,17 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
816D2CF81E07309200300E28 /* MockURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = "<group>"; };
816D2CFC1E07572500300E28 /* NetworkResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkResponse.swift; sourceTree = "<group>"; };
816D2CFE1E07573000300E28 /* JSONResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; };
816D2D001E0760B100300E28 /* MockSessionDataTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockSessionDataTask.swift; sourceTree = "<group>"; };
818A20551E05EECF00B835F6 /* NetworkOperation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NetworkOperation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
818A20581E05EECF00B835F6 /* NetworkOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NetworkOperation.h; sourceTree = "<group>"; };
818A20591E05EECF00B835F6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
818A205E1E05EECF00B835F6 /* NetworkOperationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NetworkOperationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
818A20631E05EECF00B835F6 /* NetworkOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkOperationTests.swift; sourceTree = "<group>"; };
818A20651E05EECF00B835F6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
818A206F1E05EF5F00B835F6 /* NetworkOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkOperation.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -50,6 +60,15 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
816D2D021E0760DA00300E28 /* Mocking */ = {
isa = PBXGroup;
children = (
816D2CF81E07309200300E28 /* MockURLSession.swift */,
816D2D001E0760B100300E28 /* MockSessionDataTask.swift */,
);
name = Mocking;
sourceTree = "<group>";
};
818A204B1E05EECF00B835F6 = {
isa = PBXGroup;
children = (
Expand All @@ -73,6 +92,9 @@
children = (
818A20581E05EECF00B835F6 /* NetworkOperation.h */,
818A20591E05EECF00B835F6 /* Info.plist */,
818A206F1E05EF5F00B835F6 /* NetworkOperation.swift */,
816D2CFC1E07572500300E28 /* NetworkResponse.swift */,
816D2CFE1E07573000300E28 /* JSONResponse.swift */,
);
path = NetworkOperation;
sourceTree = "<group>";
Expand All @@ -81,6 +103,7 @@
isa = PBXGroup;
children = (
818A20631E05EECF00B835F6 /* NetworkOperationTests.swift */,
816D2D021E0760DA00300E28 /* Mocking */,
818A20651E05EECF00B835F6 /* Info.plist */,
);
path = NetworkOperationTests;
Expand Down Expand Up @@ -199,14 +222,19 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
818A20701E05EF5F00B835F6 /* NetworkOperation.swift in Sources */,
816D2CFD1E07572500300E28 /* NetworkResponse.swift in Sources */,
816D2CFF1E07573000300E28 /* JSONResponse.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
818A205A1E05EECF00B835F6 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
816D2CF91E07309200300E28 /* MockURLSession.swift in Sources */,
818A20641E05EECF00B835F6 /* NetworkOperationTests.swift in Sources */,
816D2D011E0760B100300E28 /* MockSessionDataTask.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
20 changes: 20 additions & 0 deletions NetworkOperation/JSONResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// JSONNetworkResponse.swift
// NetworkOperation
//
// Created by Alasdair Law on 18/12/2016.
// Copyright © 2016 Alasdair Law. All rights reserved.
//

public struct JSONResponse: NetworkResponse {
public typealias T = Any

public let response: URLResponse
public let data: T

public init(response: URLResponse, data: Data) throws {
self.response = response

self.data = try JSONSerialization.jsonObject(with: data)
}
}
125 changes: 125 additions & 0 deletions NetworkOperation/NetworkOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//
// NetworkOperation.swift
// NetworkOperation
//
// Created by Alasdair Law on 17/12/2016.
// Copyright © 2016 Alasdair Law. All rights reserved.
//

/**
NSOperation subclass which performs a network request.
*/
public class NetworkOperation<T: NetworkResponse>: Operation {
private let session: URLSession
private let request: URLRequest
private let networkCompletion: (_ response: T?, _ error: Error?) -> Void

private var task: URLSessionTask!

private var _isFinished: Bool
override public var isFinished: Bool {
get {
return self._isFinished
}
set {
self.willChangeValue(forKey: NSStringFromSelector(#selector(setter: isFinished)))
self._isFinished = newValue
self.didChangeValue(forKey: NSStringFromSelector(#selector(setter: isFinished)))
}
}

private var _isExecuting: Bool
override public var isExecuting: Bool {
get {
return self._isExecuting
}
set {
self.willChangeValue(forKey: NSStringFromSelector(#selector(setter: isExecuting)))
self._isExecuting = newValue
self.didChangeValue(forKey: NSStringFromSelector(#selector(setter: isExecuting)))
}
}

override public var isAsynchronous: Bool {
return true
}

/**
Initialises a NetworkOperation.

- Parameters:
- request: The request the network operation should perform.
- completion: Block called after the network request has completed, or when the operation was cancelled.
*/
init(session: URLSession, request: URLRequest, completion: @escaping (_ response: T?, _ error: Error?) -> Void) {
self.session = session
self.networkCompletion = completion
self._isFinished = false
self._isExecuting = false

self.request = request

super.init()

self.task = self.session.dataTask(with: self.request) { [unowned self] (data, response, error) in
try? self.handle(data: data, response: response, error: error, nr: T.self)
}
}

/**
Initialises a NetworkOperation.

- Parameters:
- url: The URL the network operation should point to.
- completion: Block called after the network request has completed, or when the operation was cancelled.
*/
convenience init(session: URLSession, url: URL, completion: @escaping (_ response: T?, _ error: Error?) -> Void) {
self.init(session: session, request: URLRequest(url: url), completion: completion)
}

override public func start() {
if self.isCancelled {
self.isFinished = true
} else if !self.isExecuting {
self.isExecuting = true

self.task.resume()
}
}

override public func cancel() {
self.isFinished = true

self.task.cancel()

super.cancel()
}

// MARK: Private

private func handle(data: Data?, response: URLResponse?, error: Error?, nr: T.Type) throws {
var networkResponse: T?
defer {
self.completeNetworkOperation(response: networkResponse, error: error)
}

guard let response = response as? HTTPURLResponse else {
return
}
guard let data = data else {
return
}
networkResponse = try nr.init(response: response, data: data)
}

private func completeNetworkOperation(response: T?, error: Error?) {
if !self.isCancelled {
self.networkCompletion(response, error)
}

if self.isExecuting {
self.isExecuting = false
self.isFinished = true
}
}
}
16 changes: 16 additions & 0 deletions NetworkOperation/NetworkResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// NetworkResponse.swift
// NetworkOperation
//
// Created by Alasdair Law on 18/12/2016.
// Copyright © 2016 Alasdair Law. All rights reserved.
//

public protocol NetworkResponse {
associatedtype T

var response: URLResponse { get }
var data: T { get }

init(response: URLResponse, data: Data) throws
}
53 changes: 53 additions & 0 deletions NetworkOperationTests/MockSessionDataTask.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// MockSessionDataTask.swift
// NetworkOperation
//
// Created by Alasdair Law on 19/12/2016.
// Copyright © 2016 Alasdair Law. All rights reserved.
//

import Foundation

internal protocol MockResponse {
static func response(from url: URL) -> CompletionValues
}

internal enum MockSessionDataTaskError: Error {
case cancelled
}

internal class MockSessionDataTask: URLSessionDataTask {
private let mockResponse: CompletionValues
private let completion: Completion

private var _state: URLSessionTask.State
override internal var state: URLSessionTask.State {
get {
return self._state
}
set {
self._state = newValue
}
}

init(mockResponse: CompletionValues, completion: @escaping Completion) {
self.mockResponse = mockResponse
self.completion = completion
self._state = .running

super.init()
}

override internal func resume() {
let response = self.mockResponse

if self._state == .running {
self.completion(response.0, response.1, response.2)
}
}

override internal func cancel() {
self.state = .canceling
self.completion((nil, nil, MockSessionDataTaskError.cancelled))
}
}
21 changes: 21 additions & 0 deletions NetworkOperationTests/MockURLSession+JSON.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// MockURLSession+JSON.swift
// NetworkOperation
//
// Created by Alasdair Law on 18/12/2016.
// Copyright © 2016 Alasdair Law. All rights reserved.
//

import Foundation

struct MockJSONResponse: MockResponse {
static func response(from url: URL) -> (Data?, URLResponse?, Error?) {
let jsonObject = [
"test": "success"
]
let data = try! JSONSerialization.data(withJSONObject: jsonObject, options: [])
let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "https", headerFields: nil)

return (data, response, nil)
}
}
31 changes: 31 additions & 0 deletions NetworkOperationTests/MockURLSession.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// MockURLSession.swift
// NetworkOperation
//
// Created by Alasdair Law on 18/12/2016.
// Copyright © 2016 Alasdair Law. All rights reserved.
//

import Foundation

internal typealias CompletionValues = (Data?, URLResponse?, Error?)
internal typealias Completion = (CompletionValues) -> Void

internal class MockSession: URLSession {
private var mocks = [URL: CompletionValues]()

internal func mock(url: URL, with response: (URL) -> CompletionValues) {
self.mocks[url] = response(url)
}

internal func removeMocks() {
self.mocks.removeAll()
}

override internal func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
guard let mockResponse = self.mocks[request.url!] else {
return super.dataTask(with: request, completionHandler: completionHandler)
}
return MockSessionDataTask(mockResponse: mockResponse, completion: completionHandler)
}
}
Loading

0 comments on commit 30a49e4

Please sign in to comment.