I've been traditionally using a pattern where NSOperation subclasses create and manage their own NSURLConnection. The NSOperation subclass is instantiated by the view controller and will do its work without bothering the controller until it has finished. When it finishes retrieving data, it executes the completion block supplied by the view controller.
- ViewController instantiates NSOperation subclass (which encapsulates URL, parameters etc)
- NSOperation subclass instanciates NSURLConnection (which performs synchronous request and retrieves data)
- NSURLConnection dumps data to NSOperation-subclass
- NSOperation-subclass executes the completion block supplied by the view controller.
I'm trying to implement the same pattern with NSURLSession now. I want to be able to encapsulate the url and parameters required to make a network request inside a single object. Do I achieve this using NSURLSession subclasses or NSURLSessionTask subclasses?
I like to create separate classes for every network operation based on the actor design pattern.
You can use the same pattern, replacing NSURLConnection
with NSURLSessionTask
subclasses (e.g. NSURLSessionDataTask
).
As @CouchDeveloper suggests in the comments, an alternative is to wrap NSURLSessionTask
in an non-NSOperation
object with asynchronous semantics (cancel, resume, etc.). This wrapper object would do little more than encode and decode parameters, delegating most operations to the wrapped task.
In either case, to instantiate NSURLSessionTask
, you will need a NSURLSession
. (NSURLSession
is the NSURLSessionTask
factory.) If all of your operations use the same configuration (cookies, proxy, caching, etc.), you can simply use the shared session (+[NSURLSession sharedSession]
). If they need different configurations, you'll have to give them a NSURLSession
or enough information to create their own.
The NSURLSessionTask
class (and its subclasses) look a bit like operations, but they're not. Thus, you can remove operations from your code as you transition to NSURLSession
, but if you do, you will lose certain NSOperation
functionality (dependencies, controlling degree of concurrency, etc.). I'm not sure why you'd want to excise operations from your code as you transition to NSURLSession
. Personally, anywhere I used to wrap a NSURLConnection
in an operation, I now wrap a NSURLSessionTask
with an operation.
As an aside, one of the significant annoyances with NSURLSession
, though, is that the task delegates are set at the session object. We can make guesses why Apple did that, but it has all sorts of unfortunate implications. Clearly you can get around this by using the block based factory methods for creating your tasks, but then you lose the richness of the delegate API, if you happened to need that.
The implication of this is that if using block-based task factory methods, wrapping the task in a concurrent NSOperation
subclass is fairly obvious. But, if using the delegate-based tasks, though, if you want custom handlers for the tasks, you have to go through some silliness with maintaining a mapping between task identifiers and the appropriate completion blocks (which I personally put in a session manager object that I used to wrap the NSURLSession
). (FYI, I believe an implementation like this is expected in a forthcoming AFNetworking update, too. See the latter part of the discussion on Issue 1504 on the AFNetworking github site.)
Anyway, others have answered the question how you could replace your operation-based NSURLConnection
code with non-operation-based NSURLSession
code, but I'd personally suggest staying with operations.
By the way, I've uploaded a example implementation of an operation-based NSURLSession
implementation on github: https://github.com/robertmryan/NetworkManager
This is not intended to be a complete solution, but illustrates the idea of how you might implement a delegate-based NSURLSession
with NSOperation
subclasses.
I want to be able to encapsulate the url and parameters required to make a network request inside a single object. Do I achieve this using NSURLSession subclasses or NSURLSessionTask subclasses?
What you are describing is NSURLRequest
. Both NSURLSession
and NSURLConnection
take NSURLRequest
s to perform network connections (the NSURLRequest
is what it's performing). What you seem to want is a set of factory methods for generating different, specialized NSURLRequests
. For example, one that describes the "get my mail" request.
You can do this easily by creating a category on NSURLRequest
itself. Example:
@implementation NSURLRequest (Mail)
+ (instancetype) mailRequestForUser:(NSString *)user {
NSURLRequest *result = nil;
result = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"someServer""]];
// set user in a header, whatever
// return immutable copy
return [result copy];
}
@end
Create something similar to the above that meets your needs. The resulting NSURLRequest
s can then be used to create connections. In Cocoa this is a very typical pattern for customizing objects, instead of subclassing. Subclassing is pretty rare - instead Cocoa has other mechanisms for customizing class behavior, from factory methods ("convenience methods" like the above) to delegation (where an object is given responsibility for the behavior of another object).
What I did - is just allow Operation
and URLSessionTask
remain separately. For doing this I made general purpose asynchronous block operation which is used to instantiate, resume, cancel URLSessionTask
.
So, in ViewController or Data layer I'm still using chained Operations
with URLSessionTasks
inside. Sample code below can be extended by subclassing AsynchronousBlockOperation
.
File 1. Generic asynchronous operation.
open class AsynchronousOperation: Operation {
private var lockOfProperties = NonRecursiveLock.makeDefaultLock()
private var mFinished = false
private var mExecuting = false
public override init() {
super.init()
}
/// Subclasses must launch job here.
///
/// **Note** called between willChangeValueForKey and didChangeValueForKey calls, but after property mExecuting is set.
open func onStart() {
}
/// Subclasses must cancel job here.
///
/// **Note** called immediately after calling super.cancel().
open func onCancel() {
}
/// Subclasses must release job here.
///
/// **Note** called between willChangeValueForKey and didChangeValueForKey calls,
/// but after properties mExecuting and mFinished are set.
open func onFinish() {
}
}
extension AsynchronousOperation {
public final override var isAsynchronous: Bool {
return true
}
public final override var isExecuting: Bool {
return lockOfProperties.synchronized { mExecuting }
}
public final override var isFinished: Bool {
return lockOfProperties.synchronized { mFinished }
}
}
extension AsynchronousOperation {
public final override func start() {
if isCancelled || isFinished || isExecuting {
return
}
willChangeValue(forKey: #keyPath(Operation.isExecuting))
lockOfProperties.synchronized { mExecuting = true }
onStart()
didChangeValue(forKey: #keyPath(Operation.isExecuting))
}
public final override func cancel() {
super.cancel()
if isExecuting {
onCancel()
finish()
} else {
onCancel()
lockOfProperties.synchronized {
mExecuting = false
mFinished = true
}
}
}
public final func finish() {
willChangeValue(forKey: #keyPath(Operation.isExecuting))
willChangeValue(forKey: #keyPath(Operation.isFinished))
lockOfProperties.synchronized {
mExecuting = false
mFinished = true
}
onFinish()
didChangeValue(forKey: #keyPath(Operation.isExecuting))
didChangeValue(forKey: #keyPath(Operation.isFinished))
}
}
File 2. Asynchronous block based operation:
open class AsynchronousBlockOperation: AsynchronousOperation {
public typealias WorkItemType = OperationCancelationType & OperationResumingType
public typealias FinaliseBlock = () -> Void
public typealias WorkItemBlock = (@escaping FinaliseBlock) -> WorkItemType?
private var executionBlock: WorkItemBlock
private var blockExecutionQueue: DispatchQueue?
private var workItemToken: WorkItemType?
public init(blockExecutionQueue: DispatchQueue? = nil, executionBlock: @escaping WorkItemBlock) {
self.blockExecutionQueue = blockExecutionQueue
self.executionBlock = executionBlock
super.init()
}
open override func onStart() {
if let queue = blockExecutionQueue {
queue.async {
self.execute()
}
} else {
execute()
}
}
open override func onCancel() {
workItemToken?.cancelOperation()
}
private func execute() {
workItemToken = executionBlock { [weak self] in
self?.finish()
}
if var token = workItemToken {
token.resumeOperation()
} else {
finish()
}
}
}
File 3. Protocols
public protocol OperationCancelationType {
mutating func cancelOperation()
}
public protocol OperationResumingType {
mutating func resumeOperation()
}
extension URLSessionTask: OperationCancelationType {
public func cancelOperation() {
cancel()
}
}
extension URLSessionTask: OperationResumingType {
public func resumeOperation() {
resume()
}
}
Usage:
let operation = AsynchronousBlockOperation { [weak self] finalise in
return session.dataTask(with: url) {
...
finalise() // This will finish operation
}
}