A sub-classable NSOperation subclass useful for NSOperationQueues connected by adapter blocks, followed by a convenience NSOperationQueue category to wire your custom MPC_NSOperation and NSBlockOperations together.
// | |
// MPC_NSOperation.h | |
// | |
// Created by Michael Critchley on 2017/11/14. | |
// Copyright © 2017 Michael Critchley. All rights reserved. | |
//***************** | |
//This is the parent class for all MPC_NSOperation objects. | |
//To use: | |
//1. Subclass this method | |
//2. Implement the -(void)start {} method | |
//3. Subclasses must call if (![super initializeExecution]) return; at the top of the start method | |
//This is because the parent class checks and cancels execution if previous MPC_NSOperation op was cancelled | |
//4. **IMPORTANT** | |
//Subclasses must call [super completeOperation] when the operation work is done so queue can be removed | |
//Even if you call [myOp cancel], you mus still call [super completeOperation] somewhere in the executing code | |
//or you will experience more blockage than eating a pound of fiber. | |
//5. **IMPORTANT** | |
//If using blockOperationWithBlock^() as an intermediate step between two MPC_NSOperations, | |
//you should forward error objects, and you must manually check if the previous block | |
//was cancelled, and if so, cancel the next in the queue. For | |
//example, if between MPC_NSOperation1 and MPC_NSOperation2 we put a block... | |
// NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{ | |
// if (MPC_Operation1.isCancelled) { | |
// [MPC_Operation2 cancel]; | |
// } | |
// MPC_Operation2.error = MPC_Operation1.error; | |
// ... | |
// } | |
// [blockOp addDependency:MPC_Operation1]; | |
// [MPC_Operation2 addDependency:blockOp]; | |
// | |
// | |
//6. Convenience method available for creating depency chains of ops | |
// Import the "NSOperationQueue+MPC_NSOperationQueue.h" file | |
// Package IN ORDER OF EXECUTION all MPC_NSOperation instances or blockOperations in an NSArray | |
// Then call the following method on your NSOperationQueue instance | |
// [myOpqueue addDependenciesAndDefaultAdapterBlocksBetweenMPC_NSOperationsArray:myOperationsArray]; | |
// This will 1) Add intermediate blocks between MPC_NSOperations to pass forward any errors | |
// 2) Set a chain of dependencies between all objects | |
// 3) Add all operations to the operationQueue instance that called the method, which begins execution | |
//7. Call to [self errorWithMessage:] to pass a custom error message if there is some unexpected break in variables being passed forward by the proceding class. The error creation and setting will be handled in the parent class. | |
//***************** | |
#import <Foundation/Foundation.h> | |
@import CloudKit; | |
@interface MPC_NSOperation : NSOperation | |
+ (instancetype)MPC_Operation; | |
- (BOOL)initializeExecution; | |
- (void)completeOperation; | |
//Flags used internally (.executing and .finished are readOnly) | |
@property (nonatomic, assign) BOOL nowExecuting; | |
//@property (atomic, assign) BOOL nowExecuting; | |
@property (atomic, assign) BOOL nowFinished; | |
//All MPC_NSOperations should present an error on error | |
@property (strong, atomic) NSError *error; | |
@end |
// | |
// MPC_NSOperation.m | |
// | |
// Created by Michael Critchley on 2017/11/14. | |
// Copyright © 2017 Michael Critchley. All rights reserved. | |
#import "MPC_NSOperation.h" | |
@interface MPC_NSOperation () | |
@property (assign, nonatomic) BOOL isFinishedParentHolderProperty; | |
@property (assign, nonatomic) BOOL isExecutingHolderProperty; | |
@end | |
@implementation MPC_NSOperation | |
#pragma mark - Factory init | |
+ (instancetype)MPC_Operation | |
{ | |
return [[self alloc]init]; | |
} | |
- (instancetype)init | |
{ | |
self = [super init]; | |
return self; | |
} | |
#pragma mark - Implementations for subclasses | |
- (BOOL)initializeExecution //A required call from subclasses | |
{ | |
//1. Check if dependency[0] was cancelled | |
for (NSOperation *op in [self dependencies]) { | |
//2. If dependency was cancelled, also cancel this op | |
if (op.isCancelled) | |
[self cancel]; | |
} | |
//3. If we are cancelled, set this op as finished | |
if ([self isCancelled]) | |
{ | |
//4. Inform the op that we are going to change state | |
[self willChangeValueForKey:@"isFinished"]; | |
//5. Set the local holder flags | |
self.nowFinished = YES; | |
//6. Inform the op to call to get the current status from the official getters | |
[self didChangeValueForKey:@"isFinished"]; | |
//7. Inform the subclass to NOT continue with its workblock | |
return NO; | |
} | |
//8. If the operation is not canceled, inform the op we will change state. | |
[self willChangeValueForKey:@"isExecuting"]; | |
//9. Set the local holder flag status | |
self.nowExecuting = YES; | |
//10. Inform the op to call to get the current execution status from the official getters | |
[self didChangeValueForKey:@"isExecuting"]; | |
//11. Inform the subclass to continue with its workblock | |
return YES; | |
} | |
//Delegate getters | |
- (BOOL)isExecuting { | |
return self.nowExecuting; | |
} | |
- (BOOL)isFinished { | |
return self.nowFinished; | |
} | |
- (BOOL)isAsynchronous { | |
return YES; | |
} | |
//Setters that implement subclass property setting via self. | |
- (void)setNowFinished:(BOOL)nowFinished | |
{ | |
self.isFinishedParentHolderProperty = nowFinished; | |
} | |
- (BOOL)nowFinished | |
{ | |
return self.isFinishedParentHolderProperty; | |
} | |
- (void)setNowExecuting:(BOOL)nowExecuting | |
{ | |
self.isExecutingHolderProperty = nowExecuting; | |
} | |
- (BOOL)nowExecuting | |
{ | |
return self.isExecutingHolderProperty; | |
} | |
//Mark the subclassed operation as complete | |
- (void)completeOperation { | |
[self willChangeValueForKey:@"isFinished"]; | |
[self willChangeValueForKey:@"isExecuting"]; | |
self.nowExecuting = NO; | |
self.nowFinished = YES; | |
[self didChangeValueForKey:@"isExecuting"]; | |
[self didChangeValueForKey:@"isFinished"]; | |
} | |
#pragma mark - Error handling | |
- (void)exposeErrorWithMessage:(NSString *)message | |
{ | |
if (!message) | |
message = @""; | |
self.error = [[NSError alloc]initWithDomain:@"MPC_NSOperationDomain" | |
code:-1 | |
userInfo:@{NSLocalizedDescriptionKey : message, | |
NSLocalizedFailureReasonErrorKey : @"Likely issue with required parameters being passed as nil (or not at all)", | |
NSLocalizedRecoverySuggestionErrorKey : @"Use adapter blocks (blockOperationWithBlock^() between operations to set variables on upcoming operations. See MPC_NSOperation.h for more."}]; | |
} | |
@end |
// | |
// NSOperationQueue+MPC_NSOperationQueue.h | |
// | |
// Created by Michael Critchley on 2017/11/15. | |
// Copyright © 2017 Michael Critchley. All rights reserved. | |
/***************** | |
This category has a convenience instance method for NSOperationQueue that packages | |
MPC_NSOperation subclasses and intermediate adapter blocks. Specificially, the method | |
1. Puts a small, intermediate block of code between two MPC_NSOperation subclasses | |
to carry forward errors and .isCancelled state. | |
2. Creates a depenency chain starting from array[0] down to array[n] | |
3. Adds the operations to the receiving NSOperationQueue object that called the method | |
** Do not use this with regular NSOperation objects. It will break. Use MPC_NSOperation objects only | |
** Add your ops to your array in the order they should be performed | |
** You can add blockOperationWithBlock^() operations, but in the code you write in the | |
block, be sure to pass forward the error and is.Cancelled state information to the | |
next op in the queue (See MPC_NSOperation.h) for an example | |
******************/ | |
#import <Foundation/Foundation.h> | |
@interface NSOperationQueue (MPC_NSOperationQueue) | |
//CREATES AND ADDS TO queue a dependency chain starting from array[0] (ie, array[0]will be the first op performed | |
//If array[i] and array[i+1] are both subclasses of MPC_NSOperation, it will add a default NSBlockOperation | |
//that passes errors forward. | |
- (void )addDependenciesAndDefaultAdapterBlocksBetweenMPC_NSOperationsArray:(NSArray *)operationsArray; | |
@end |
// | |
// NSOperationQueue+MPC_NSOperationQueue.m | |
// | |
// Created by Michael Critchley on 2017/11/15. | |
// Copyright © 2017 Michael Critchley. All rights reserved. | |
// | |
#import "NSOperationQueue+MPC_NSOperationQueue.h" | |
#import "MPC_NSOperation.h" | |
@implementation NSOperationQueue (MPC_NSOperationQueue) | |
- (void)addDependenciesAndDefaultAdapterBlocksBetweenMPC_NSOperationsArray:(NSArray *)operationsArray | |
{ | |
if (!operationsArray) return; | |
if (operationsArray.count < 2) return; | |
//1. Add default blocks to pass forward errors / isCancelled | |
NSArray *arrayWithBlockOps = [NSOperationQueue _arrayWithAddedDefaultNSBlockOperationsFromOperationsArray:operationsArray]; | |
//2. Set dependencies | |
NSArray *arrayWithDependencies = [self configureDependenciesForNSOperationsArray:arrayWithBlockOps]; | |
//3. Add all of these ops to the operationQueue | |
[self addOperations:arrayWithDependencies waitUntilFinished:NO]; | |
} | |
+ (NSArray *)_arrayWithAddedDefaultNSBlockOperationsFromOperationsArray:(NSArray *)operationsArray | |
{ | |
//1. Create iVars | |
Class checkClass = [MPC_NSOperation class]; | |
NSMutableArray *holder = [NSMutableArray new]; | |
//2. Iterate through initial array to add small adapter blocks that will | |
//pass the error object and .isCancelled flag from Op1 (before block) over to Op2 (after block) | |
for (NSOperation *op in operationsArray) { | |
//3. Add each object from original array | |
[holder addObject:op]; | |
//4. End if we are on the final object | |
if ([op isEqual:operationsArray.lastObject]) | |
break; | |
//5. Check if both this object and the next are MPC_Operation subclasses | |
NSInteger index1 = [operationsArray indexOfObject:op]; | |
NSOperation *op2 = operationsArray[index1 + 1]; | |
if ([op isKindOfClass:checkClass] && | |
[op2 isKindOfClass:checkClass]) { | |
//6. If so, create a block that forwards the state variables of Op1 to Op2 | |
NSBlockOperation *passForward = [NSBlockOperation blockOperationWithBlock:^{ | |
//7. Cancel subsequent op if previous was in a cancelled state | |
if (op.isCancelled) | |
[op2 cancel]; | |
//8. Pass forward errors (nil or otherwise) | |
((MPC_NSOperation *)op2).error = ((MPC_NSOperation *)op).error; | |
}]; | |
//9. Add the block to the holder | |
[holder addObject:passForward]; | |
} | |
} | |
return [holder copy]; | |
} | |
- (NSArray *)configureDependenciesForNSOperationsArray:(NSArray *)operationsArray | |
{ | |
NSInteger i = 0; | |
for (NSOperation *op in operationsArray) { | |
//End if we are on the final object | |
if ([op isEqual:operationsArray.lastObject]) | |
break; | |
//Add a depency from the earlier op to the next one in the chain | |
NSInteger index1 = [operationsArray indexOfObject:op]; | |
NSOperation *op2 = operationsArray[index1 + 1]; | |
[op2 addDependency:op]; | |
i++; | |
} | |
return operationsArray; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment