Resetting NSOperation - ios

My network requests need to happen in a FIFO serial fashion. If one fails due to a network issue (i.e. offline, timeout) I need the failed request to retry before continuing the queue. How can I accomplish this?
When the operation fails, I set executing and finished both to - assuming that would work as a 'reset' for the NSOperation. But the queue never resumes.
Is there something I'm missing?

You could build a network operation that performs retries, and only sets isFinished when the network completion block is called, and it determines that no retry is needed:
class NetworkOperationWithRetry: AsynchronousOperation {
var session: NSURLSession
var request: NSURLRequest
var task: NSURLSessionTask?
var networkCompletionHandler: ((NSData?, NSURLResponse?, NSError?) -> ())?
init(session: NSURLSession = NSURLSession.sharedSession(), request: NSURLRequest, networkCompletionHandler: (NSData?, NSURLResponse?, NSError?) -> ()) {
self.session = session
self.request = request
self.networkCompletionHandler = networkCompletionHandler
}
override func main() {
attemptRequest()
}
func attemptRequest() {
print("attempting \(request.URL!.lastPathComponent)")
task = session.dataTaskWithRequest(request) { data, response, error in
if error?.domain == NSURLErrorDomain && error?.code == NSURLErrorNotConnectedToInternet {
print("will retry \(self.request.URL!.lastPathComponent)")
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 * Int64(NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)) {
self.attemptRequest()
}
return
}
print("finished \(self.request.URL!.lastPathComponent)")
self.networkCompletionHandler?(data, response, error)
self.networkCompletionHandler = nil
self.completeOperation()
}
task?.resume()
}
override func cancel() {
task?.cancel()
super.cancel()
}
}
And you could call it like so:
let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 1 // I wouldn't generally do this, but just to illustrate that it's honoring operation queue dependencies/concurrency settings
let requests = urlStrings.map { NSURLRequest(URL: NSURL(string: $0)!) }
requests.forEach { request in
queue.addOperation(NetworkOperationWithRetry(request: request) { data, response, error in
// do something with the `data`, `response`, and `error`
})
}
Now, that's only retrying on NSURLErrorNotConnectedToInternet, but you could change that logic to be whatever you want. Likewise, I'd probably be inclined to add some "max retries" logic. Also, if the lack of Internet connectivity is really the issue, rather than retrying until Internet connectivity is achieved, I'd probably be inclined to set the operation's isReady notification tied into Reachability.
By the way, the above uses the following AsynchronousOperation:
/// Asynchronous Operation base class
///
/// This class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
/// necessary and then ensuring that `completeOperation()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `completeOperation()` is called.
public class AsynchronousOperation : NSOperation {
override public var asynchronous: Bool { return true }
private let stateLock = NSLock()
private var _executing: Bool = false
override private(set) public var executing: Bool {
get {
return stateLock.withCriticalScope { _executing }
}
set {
willChangeValueForKey("isExecuting")
stateLock.withCriticalScope { _executing = newValue }
didChangeValueForKey("isExecuting")
}
}
private var _finished: Bool = false
override private(set) public var finished: Bool {
get {
return stateLock.withCriticalScope { _finished }
}
set {
willChangeValueForKey("isFinished")
stateLock.withCriticalScope { _finished = newValue }
didChangeValueForKey("isFinished")
}
}
/// Complete the operation
///
/// This will result in the appropriate KVN of isFinished and isExecuting
public func completeOperation() {
if executing {
executing = false
}
if !finished {
finished = true
}
}
override public func start() {
if cancelled {
finished = true
return
}
executing = true
main()
}
override public func main() {
fatalError("subclasses must override `main`")
}
}
extension NSLock {
/// Perform closure within lock.
///
/// An extension to `NSLock` to simplify executing critical code.
///
/// - parameter block: The closure to be performed.
func withCriticalScope<T>(#noescape block: Void -> T) -> T {
lock()
let value = block()
unlock()
return value
}
}

I used this custom class to manage NSOperation. I fiddled a bit with the code from GCDWebServer and came out with this stub. Most of it is self explanatory.
#interface SROperation : NSOperation <NSCopying>
///-------------------------------
/// #name error reporting
///-------------------------------
/**
The error, if any, that occurred in the lifecycle of the request.
*/
#property (nonatomic, strong) NSError *error;
///-----------------------------------------
/// #name the operation properties
///-----------------------------------------
/**
a dictionary.
*/
#property (nonatomic, strong) NSDictionary*aDictionary;
///----------------------------------
/// #name Pausing / Resuming Requests
///----------------------------------
+(instancetype)operationWithDictionary:(NSDictionary*)dict;
/**
Pauses the execution of the request operation.
A paused operation returns `NO` for `-isReady`, `-isExecuting`, and `-isFinished`. As such, it will remain in an `NSOperationQueue` until it is either cancelled or resumed. Pausing a finished, cancelled, or paused operation has no effect.
*/
- (void)pause;
/**
Whether the request operation is currently paused.
#return `YES` if the operation is currently paused, otherwise `NO`.
*/
#property (NS_NONATOMIC_IOSONLY, getter=isPaused, readonly) BOOL paused;
/**
Resumes the execution of the paused request operation.
Pause/Resume behavior varies depending on the underlying implementation for the operation class. In its base implementation, resuming a paused requests restarts the original request. However, since HTTP defines a specification for how to request a specific content range, `AFHTTPRequestOperation` will resume downloading the request from where it left off, instead of restarting the original request.
*/
- (void)resume;
#end
SROperation.m
#import "SROperation.h"
typedef NS_ENUM(NSInteger, SROperationState) {
SROperationPausedState = -1,
SROperationReadyState = 1,
SROperationExecutingState = 2,
SROperationFinishedState = 3,
};
static NSString * const kSROperationLockName = #"your.application.bundle.id.operationlock"
static inline BOOL SRStateTransitionIsValid(SROperationState fromState, SROperationState toState, BOOL isCancelled) {
switch (fromState) {
case SROperationReadyState:
switch (toState) {
case SROperationPausedState:
case SROperationExecutingState:
return YES;
case SROperationFinishedState:
return isCancelled;
default:
return NO;
}
case SROperationExecutingState:
switch (toState) {
case SROperationPausedState:
case SROperationFinishedState:
return YES;
default:
return NO;
}
case SROperationFinishedState:
return NO;
case SROperationPausedState:
return toState == SROperationReadyState;
default: {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
switch (toState) {
case SROperationPausedState:
case SROperationReadyState:
case SROperationExecutingState:
case SROperationFinishedState:
return YES;
default:
return NO;
}
}
#pragma clang diagnostic pop
}
}
static inline NSString * SRKeyPathFromSROperationState(SROperationState state) {
switch (state) {
case SROperationReadyState:
return #"isReady";
case SROperationExecutingState:
return #"isExecuting";
case SROperationFinishedState:
return #"isFinished";
case SROperationPausedState:
return #"isPaused";
default: {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
return #"state";
#pragma clang diagnostic pop
}
}
}
#interface SROperation ()
#property (readwrite, nonatomic, assign) SROperationState state;
#property (readwrite, nonatomic, strong) NSRecursiveLock *lock;
#end
#implementation SROperation
+(instancetype)operationWithDictionary:(NSDictionary*)dict {
return [[self alloc] initWithDictionary:dict];
}
-(instancetype)initWithDictionary:(NSDictionary*)aDictionary {
self = [self init];
if (self)
{
self.aDictionary = aDictionary;
self.queuePriority = NSOperationQueuePriorityVeryHigh;
}
return self;
}
-(instancetype)init
{
self = [super init];
if(self) {
_state = SROperationReadyState;
_lock = [[NSRecursiveLock alloc] init];
_lock.name = kSROperationLockName;
}
return self;
}
#pragma mark -
- (void)setState:(SROperationState)state {
if (!SRStateTransitionIsValid(self.state, state, [self isCancelled])) {
return;
}
[self.lock lock];
NSString *oldStateKey = SRKeyPathFromSROperationState(self.state);
NSString *newStateKey = SRKeyPathFromSROperationState(state);
[self willChangeValueForKey:newStateKey];
[self willChangeValueForKey:oldStateKey];
_state = state;
[self didChangeValueForKey:oldStateKey];
[self didChangeValueForKey:newStateKey];
[self.lock unlock];
}
- (void)pause {
if ([self isPaused] || [self isFinished] || [self isCancelled]) {
return;
}
[self.lock lock];
if ([self isExecuting]) {
dispatch_async(dispatch_get_main_queue(), ^{
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:SROperationDidFinishNotification object:self userInfo:nil];
});
}
self.state = SROperationPausedState;
[self.lock unlock];
}
-(void)operationDidPause
{
}
- (BOOL)isPaused {
return self.state == SROperationPausedState;
}
- (void)resume {
if (![self isPaused]) {
return;
}
[self.lock lock];
self.state = SROperationReadyState;
[self start];
[self.lock unlock];
}
- (BOOL)isReady {
return self.state == SROperationReadyState && [super isReady];
}
- (BOOL)isExecuting {
return self.state == SROperationExecutingState;
}
- (BOOL)isFinished {
return self.state == SROperationFinishedState;
}
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self cancelConnection];
} else if ([self isReady]) {
self.state = SROperationExecutingState;
[self operationDidStart];
}
[self.lock unlock];
}
- (void)operationDidStart {
[self.lock lock];
if (![self isCancelled]) {
// YOUR ACTUAL OPERATION CODE
// finish
self.state = SROperationFinishedState;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SROperationDidFinishNotification object:nil];
});
}
[self.lock unlock];
}
- (void)cancel {
[self.lock lock];
if (![self isFinished] && ![self isCancelled]) {
[super cancel];
if ([self isExecuting]) {
}
}
[self.lock unlock];
}
#pragma mark - NSObject
- (NSString *)description {
[self.lock lock];
NSString *description = [NSString stringWithFormat:#"<%#: %p, state: %#, cancelled: %#>", NSStringFromClass([self class]), self, SRKeyPathFromSROperationState(self.state), ([self isCancelled] ? #"YES" : #"NO")];
[self.lock unlock];
return description;
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
SROperation *operation = [(SROperation *)[[self class] allocWithZone:zone] init];
// copy more properties
return operation;
}
}

Related

Unexpected behaviour using Block-based callbacks with CBCentralManager / CBCentralManagerDelegate and [NSThread sleepForTimeInterval]

I am trying to write some code to scan for bluetooth devices for a while, and then return the array of discovered peripherals through a block-based callback.
Blocking code should not be an issue as the code will be called asynchronously.
After reading up on the API documentation my initial plan of attack was to write an implementation for CBCentralManagerDelegate, use an init method to give it a block-based callback to call once the CBManagerState is PoweredOn, and then initialize this class with a callback that triggers the scanning and extracts the discovered Peripherals.
The issue is... it doesn't work. Except when it does.
Now I could work out a workaround to reach my goal, but for the sake of learning and understanding I am very interested in where exactly the issue originates from.
typedef void (^SomeBlock)(CBCentralManager*);
#interface TEST : NSObject <CBCentralManagerDelegate>
#property CBCentralManager* manager;
#property SomeBlock onPoweredOn;
#property NSMutableArray<CBPeripheral*>* peripherals;
- (void) init: (SomeBlock) onPoweredOn;
- (void) startScan;
- (void) stopScan;
#end
#implementation TEST
- (void) init: (SomeBlock) onPoweredOn {
NSLog(#"%#", #"init");
self.onPoweredOn = onPoweredOn;
self.manager = [CBCentralManager alloc];
dispatch_queue_attr_t attr = DISPATCH_QUEUE_CONCURRENT;
dispatch_queue_t queue =dispatch_queue_create("BTManagerHandler", attr);
self.manager = [self.manager initWithDelegate: self queue: queue];
}
- (void) startScan {
NSLog(#"%#", #"startScan");
[self.manager scanForPeripheralsWithServices: nil options: nil];
}
- (void) stopScan {
NSLog(#"%#", #"stopScan ");
[self.manager stopScan];
}
- (void) centralManagerDidUpdateState: (nonnull CBCentralManager *) manager {
NSLog(#"%#", #"centralManagerDidUpdateState:");
switch (manager.state) {
case CBManagerStateUnknown:
NSLog(#"%#", #"CBManagerStateUnknown:");
break;
case CBManagerStateResetting:
NSLog(#"%#", #"CBManagerStateResetting:");
break;
case CBManagerStateUnsupported:
NSLog(#"%#", #"CBManagerStateUnsupported:");
break;
case CBManagerStateUnauthorized:
NSLog(#"%#", #"CBManagerStateUnauthorized:");
break;
case CBManagerStatePoweredOff:
NSLog(#"%#", #"CBManagerStatePoweredOff:");
break;
case CBManagerStatePoweredOn:
NSLog(#"%#", #"CBManagerStatePoweredOn:");
if (self.onPoweredOn != nil) self.onPoweredOn(manager);
break;
}
}
- (void) centralManager: (nonnull CBCentralManager*) central didDiscoverPeripheral: (nonnull CBPeripheral*) peripheral advertisementData: (nonnull NSDictionary<NSString*, id>*) advertisementData RSSI: (nonnull NSNumber*) RSSI {
NSLog(#"%#", #"centralManager:didDiscoverPeripheral:advertisementData:RSSI:");
if (self.peripherals == nil) self.peripherals = [NSMutableArray array];
for (CBPeripheral* _peripheral in self.peripherals) {
if (peripheral.identifier == _peripheral.identifier) return;
}
[self.peripherals addObject: peripheral];
}
#end
+ (void) discoverDevices {
TEST* test = nil;
#try {
test = [TEST alloc];
SomeBlock onPoweredOn = ^(CBCentralManager* manager) {
NSLog(#"%#", #"_onPoweredOn_");
[test startScan];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
};
[test init: onPoweredOn];
} #catch(NSException* e) {
// exception handling
} #finally {
// cleanup
}
}
I would expect the above code to work, but it doesn't.
The 'onPoweredOn' callback and the 'startScan' method are called correctly, but the 'centralManager:didDiscoverPeripheral:advertisementData:RSSI:' method is never called.
After some trial and error I found that the following works:
+ (void) discoverDevices {
TEST* test = nil;
#try {
test = [TEST alloc];
SomeBlock onPoweredOn = ^(CBCentralManager* manager) {
NSLog(#"%#", #"_onPoweredOn_");
[test startScan];
};
[test init: onPoweredOn];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
} #catch(NSException* e) {
// exception handling
} #finally {
// cleanup
}
}
After some more trial and error I narrowed it down to one line of code:
+ (void) discoverDevices {
TEST* test = nil;
#try {
test = [TEST alloc];
SomeBlock onPoweredOn = ^(CBCentralManager* manager) {
NSLog(#"%#", #"_onPoweredOn_");
[test startScan];
[NSThread sleepForTimeInterval: 10.0]; // <<=== this line! ===
};
[test init: onPoweredOn];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
} #catch(NSException* e) {
// exception handling
} #finally {
// cleanup
}
}
This suggests that that using a [NSThread sleepForTimeInterval:] blocks the discovery of bluetooth devices... but tat seems illogical to me because the same code works without the block-based callback:
+ (void) discoverDevices {
TEST* test = nil;
#try {
test = [TEST alloc];
[test init: nil];
[NSThread sleepForTimeInterval: 1.0];
[test startScan];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
} #catch(NSException* e) {
// exception handling
} #finally {
// cleanup
}
}
Conclusion: combining CBCentralManager, block-based callbacks and [NSThread sleepForTimeInterval:] leads to unexpected behaviour??
but why? what's so special about this specific combination?

Why is NSOperation::completionBlock called too soon?

I have a very simple asynchronous operation
#interface IDBWebpValidationOperation()
#property BOOL executing;
#property BOOL finished;
#end
#implementation IDBWebpValidationOperation
#synthesize executing;
#synthesize finished;
- (instancetype)init
{
self = [super init];
if (self) {
self.completionBlock = ^{
NSDLog(#"webp validation has finished");
};
}
return self;
}
- (void)main
{
IDBAssert0(self.bestCapture.webpCandidate);
self.finished = NO;
if(self.postProcessingValidator) {
self.executing = YES;
// this starts async operation, see callback below
self.postProcessingValidator(self.bestCapture.webpCandidate);
}else{
IDBAssert0(0);
// self.bestCapture.jpegNSData = self.bestCapture.webpCandidate;
IDBAssert0(self.bestCapture.jpegNSData);
self.executing = NO;
self.finished = YES;
}
}
- (void)scanningViewController: (UIViewController<PPScanningViewController>*)scanningViewController
didOutputResults:(NSArray*)results
{
if([results count]>0) {
self.bestCapture.jpegNSData = self.bestCapture.webpCandidate;
IDBAssert0(self.bestCapture.jpegNSData);
}else{
IDBAssert0(self.microblinkFailureHandler);
self.microblinkFailureHandler();
}
IDBAssert0(!self.finished);
self.executing = NO;
self.finished = YES;
}
-(BOOL)isAsynchronous
{
return YES; //Default is NO so overriding it to return YES;
}
Here I have synthesized two atomic properties executing & finished
to avoid lots of (in my opinion dumb & extraneous) code swift would force you to do for KVO and the synthesided atomic properties would get me out of the box
(I think).
The issue is completionBlock is called before scanningViewController:
callback is invoked. Why?????
This is on ios 9.3.x in case this matters

Hold NSOperationQueue until previous operation completes

I want to perform few operations and need to start the next operation only upon completion of the previous one. The operation I'm adding will send async call to the server and receive data. I want to start the next operation only after the first call to the server finish receiving data from the server. How to do that?
{....
PhotoDownloader *pd = [[PhotoDownloader alloc] init];
[GetGlobalOperationQueue addOperation:pd];
}
Inside the PhotoDownloader I will allocate the required parameters and call a Global function which handles all the request
[GlobalCommunicationUtil sendServerReq:reqObj withResponseHandler:self];
Inside the sendServerReq method I will construct the URL request and send it to the server and this call is a "sendAsynchronousRequest" call. The PhotoDownloader will have the CommunicationUtil's delegate methods.
There are two parts to this question:
You asked:
How do I make one operation not start until the previous operation finishes?
To do this, you could, theoretically, simply make a serial queue (which is fine if you want to make all operations wait until the prior one finishes). With an NSOperationQueue, you achieve that simply by setting maxConcurrentOperationCount to 1.
Or, a little more flexible, you could establish dependencies between operations where dependencies are needed, but otherwise enjoy concurrency. For example, if you wanted to make two network requests dependent upon the completion of a third, you could do something like:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4; // generally with network requests, you don't want to exceed 4 or 5 concurrent operations;
// it doesn't matter too much here, since there are only 3 operations, but don't
// try to run more than 4 or 5 network requests at the same time
NSOperation *operation1 = [[NetworkOperation alloc] initWithRequest:request1 completionHandler:^(NSData *data, NSError *error) {
[self doSomethingWithData:data fromRequest:request1 error:error];
}];
NSOperation *operation2 = [[NetworkOperation alloc] initWithRequest:request2 completionHandler:^(NSData *data, NSError *error) {
[self doSomethingWithData:data fromRequest:request2 error:error];
}];
NSOperation *operation3 = [[NetworkOperation alloc] initWithRequest:request3 completionHandler:^(NSData *data, NSError *error) {
[self doSomethingWithData:data fromRequest:request3 error:error];
}];
[operation2 addDependency:operation1]; // don't start operation2 or 3 until operation1 is done
[operation3 addDependency:operation1];
[queue addOperation:operation1]; // now add all three to the queue
[queue addOperation:operation2];
[queue addOperation:operation3];
You asked:
How do I ensure that an operation will not complete until the asynchronous network request it issued has finished as well?
Again, there are different approaches here. Sometimes you can avail yourself with semaphores to make asynchronous process synchronous. But, much better is to use a concurrent NSOperation subclass.
An "asynchronous" NSOperation is simply one that will not complete until it issues a isFinished notification (thereby allowing any asynchronous tasks it initiates to finish). And an NSOperation class specifies itself as an asynchronous operation simply by returning YES in its isAsynchronous implementation. Thus, an abstract class implementation of an asynchronous operation might look like:
// AsynchronousOperation.h
#import Foundation;
#interface AsynchronousOperation : NSOperation
/**
Complete the asynchronous operation.
If you create an asynchronous operation, you _must_ call this for all paths of execution
or else the operation will not terminate (and dependent operations and/or available
concurrent threads for the operation queue (`maxConcurrentOperationCount`) will be blocked.
*/
- (void)completeOperation;
#end
and
//
// AsynchronousOperation.m
//
#import "AsynchronousOperation.h"
#interface AsynchronousOperation ()
#property (getter = isFinished, readwrite) BOOL finished;
#property (getter = isExecuting, readwrite) BOOL executing;
#end
#implementation AsynchronousOperation
#synthesize finished = _finished;
#synthesize executing = _executing;
- (instancetype)init {
self = [super init];
if (self) {
_finished = NO;
_executing = NO;
}
return self;
}
- (void)start {
if (self.isCancelled) {
if (!self.isFinished) self.finished = YES;
return;
}
self.executing = YES;
[self main];
}
- (void)completeOperation {
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
#pragma mark - NSOperation methods
- (BOOL)isAsynchronous {
return YES;
}
- (BOOL)isExecuting {
#synchronized(self) { return _executing; }
}
- (BOOL)isFinished {
#synchronized(self) { return _finished; }
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:#"isExecuting"];
#synchronized(self) { _executing = executing; }
[self didChangeValueForKey:#"isExecuting"];
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:#"isFinished"];
#synchronized(self) { _finished = finished; }
[self didChangeValueForKey:#"isFinished"];
}
#end
Now that we have that abstract, asynchronous NSOperation subclass, we can use it in our concrete NetworkOperation class:
#import "AsynchronousOperation.h"
NS_ASSUME_NONNULL_BEGIN
typedef void(^NetworkOperationCompletionBlock)(NSData * _Nullable data, NSError * _Nullable error);
#interface NetworkOperation : AsynchronousOperation
#property (nullable, nonatomic, copy) NetworkOperationCompletionBlock networkOperationCompletionBlock;
#property (nonatomic, copy) NSURLRequest *request;
- (instancetype)initWithRequest:(NSURLRequest *)request completionHandler:(NetworkOperationCompletionBlock)completionHandler;
#end
NS_ASSUME_NONNULL_END
and
// NetworkOperation.m
#import "NetworkOperation.h"
#interface NetworkOperation ()
#property (nonatomic, weak) NSURLSessionTask *task;
#end
#implementation NetworkOperation
- (instancetype)initWithRequest:(NSURLRequest *)request completionHandler:(NetworkOperationCompletionBlock)completionHandler {
self = [self init];
if (self) {
self.request = request;
self.networkOperationCompletionBlock = completionHandler;
}
return self;
}
- (void)main {
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionTask *task = [session dataTaskWithRequest:self.request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (self.networkOperationCompletionBlock) {
self.networkOperationCompletionBlock(data, error);
self.networkOperationCompletionBlock = nil;
}
[self completeOperation];
}];
[task resume];
self.task = task;
}
- (void)cancel {
[super cancel];
[self.task cancel];
}
#end
Now, in this example, I'm using block-based implementation of these asynchronous network requests, but the idea works equally well in delegate-based connections/sessions, too. (The only hassle is that NSURLSession specifies its task-related delegate methods to be part of the session, not the network task.)
Clearly the implementation of your own NetworkOperation class may differ wildly (use delegate patterns or completion block patterns, etc.), but hopefully this illustrates the idea of a concurrent operation. For more information, see the Operation Queues chapter of the Concurrency Programming Guide, notably the section titled "Configuring Operations for Concurrent Execution".
A swift version for an asynchronous operation (which was not very obvious):
final class NetworkOperation: Operation {
lazy var session: NSURLSession = {
return NSURLSession.sharedSession()
}()
private var _finished = false {
willSet {
willChangeValue(forKey: "isFinished")
}
didSet {
didChangeValue(forKey: "isFinished")
}
}
private var _executing = false {
willSet {
willChangeValue(forKey: "isExecuting")
}
didSet {
didChangeValue(forKey: "isExecuting")
}
}
override var isAsynchronous: Bool {
return true
}
override var isFinished: Bool {
return _finished
}
override var isExecuting: Bool {
return _executing
}
override func start() {
_executing = true
execute()
}
func execute() {
task = session.downloadTaskWithURL(NSURL(string: "yourURL")!) {
(url, response, error) in
if error == nil {
// Notify the response by means of a closure or what you prefer
// Remember to run in the main thread since NSURLSession runs its
// task on background by default
} else {
// Notify the failure by means of a closure or what you prefer
// Remember to run in the main thread since NSURLSession runs its
// task on background by default
}
// Remember to tell the operation queue that the execution has completed
self.finish()
}
}
func finish() {
//Async task complete and hence the operation is complete
_executing = false
_finished = true
}
}
To serialize the operations:
let operationQueue = OperationQueue()
let operation1 = NetworkOperation()
let operation2 = NetworkOperation()
operation2.addDependency(operation1)
operationQueue.addOperations([operation1, operation2], waitUntilFinished: false)
Is it manadatory to use NSOperationQueue?
This behaviour is very easy to implement with serial queues
https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html
Assuming you have a class to manage the operations, you would create a serial dispatch queue in your init method with
queue = dispatch_queue_create("com.example.MyQueue", NULL);
And you would have a method to enqueue request, something like this
- (void) enqueueRequest:(NSURL *)requestURL
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_sync(queue, ^{ /* get data from requestURL */ }) });
}
This way, only one request is active at one time, even though each request will be executed in a separated background thread, and several requests will be enqueued until the active request finishes.

How to write a proper singleton BOOL function

I am using ASIHTTPRequest to check if user logged in, and try to return a boolean value when login successfull
Problem: When I request the boolean it always returns 0 then few seconds later ASIHTTPRequest finishes its request and update the boolean value.
What I want: get the boolean value after all requests finishes. I think proper way would be write a boolean function and retrieve the result of asihhtp requests?
in singleton:
#interface CloudConnection : NSObject
{
BOOL isUserLoggedIN;
}
#property BOOL isUserLoggedIN;
+ (CloudConnection *)sharedInstance;
#end
+ (CloudConnection *)sharedInstance
{
static CloudConnection *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[CloudConnection alloc] init];
// Do any other initialisation stuff here
});
return sharedInstance;
}
- (id)init {
if (self = [super init]) {
//send login request
[self sendLoginRequest];
}
return self;
}
-(void) sendLoginRequest{ .....}
- (void)requestFinished:(ASIHTTPRequest *)request
{ else if (request.responseStatusCode == 202) {
//parse json data
NSLog(#"Login Succesfull");
_isUserLoggedIN=YES;
}
}
- (void)requestFailed:(ASIHTTPRequest *)request{}
In a VC:
CloudConnection *sharedInstance=[CloudConnection sharedInstance];
NSLog(#"is logged in init %hhd",sharedInstance.isUserLoggedIN);
[self performSelector:#selector( checkLoginAfterFiveSeconds) withObject:nil afterDelay:5.0];
-(void) checkLoginAfterFiveSeconds
{
CloudConnection *sharedInstance=[CloudConnection sharedInstance];
NSLog(#"is logged in init %hhd",sharedInstance.isUserLoggedIN);
}
NSLOG:
is logged in init 0
Login Succesfull`
is logged in init 1 //after 5 secs
well if you do what you propose, its gonna block the calling thread. and you never want a thread waiting for network traffic especially not the main / ui thread
make it a void function and make it call a completionHandler or ... send a NSNotification once the result can be calculated directly! :)
Yes you are right :)
In your request completion block call this method:
[self loginResult:result];
-(void)loginResult:(BOOL)result
{
if(result == TRUE)
{
NSLog(#"Login successfully now call any method or do what ever you want");
}
else
{
NSLog(#"Login unsuccessfull");
}
}

Keeping an NSOperation in a queue even after it completes

Typically once the main method of the an NSOperation is completed, the op is marked completed and it is removed from the queue. However, my op makes networking calls, and I want to handle retries. How do I keep an NSOperation in an NSOperationQueue until I explicitly say it's ok to remove it?
I can't find the original source for the work I did on my current project.
I have subclassed NSOperation and do this...
Add private properties in the .m...
#property (nonatomic) BOOL executing;
#property (nonatomic) BOOL finished;
#property (nonatomic) BOOL completed;
Init the operation...
- (id)init
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
_completed = NO;
}
return self;
}
Add the functions to return the properties...
- (BOOL)isExecuting { return self.executing; }
- (BOOL)isFinished { return self.finished; }
- (BOOL)isCompleted { return self.completed; }
- (BOOL)isConcurrent { return YES; }
In the "start" function (this is the bit that the operationQueue calls...
- (void)start
{
if ([self isCancelled]) {
[self willChangeValueForKey:#"isFinished"];
self.finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:#"isExecuting"];
self.executing = YES;
[self didChangeValueForKey:#"isExecuting"];
[NSThread detachNewThreadSelector:#selector(main) toTarget:self withObject:nil];
}
Then in the main put your working code...
- (void)main
{
#try {
//this is where your loop would go with your counter and stuff
//when you want the operationQueue to be notified that the work
//is done just call...
[self completeOperation];
}
#catch (NSException *exception) {
NSLog(#"Exception! %#", exception);
[self completeOperation];
}
}
Write the code for completeOperation...
- (void)completeOperation {
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
self.executing = NO;
self.finished = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
That's it.
As long as you have these then the operation will work.
You can add as many other functions and properties as you wish.
In fact, I have actually subclassed this class as I have a function that does all the work for different types of object (it's an upload thing). I have defined a function...
- (void)uploadData
{
//subclass this method.
}
Then all I have in the subclasses is a custom "uploadData" method.
I find this really useful as it gives you fine grain control on when to finish the operation etc...

Resources