Created
June 20, 2018 19:23
-
-
Save lobianco/80ae290c8063d45b65e54b8c418e56aa to your computer and use it in GitHub Desktop.
ALBlockOperation was made for block-based operations that need to run asynchronous code in their execution blocks.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ALBlockOperation.h | |
// | |
// Created by Anthony Lobianco on 10/5/17. | |
// | |
#import <Foundation/Foundation.h> | |
NS_ASSUME_NONNULL_BEGIN | |
@class ALBlockOperation; | |
typedef void (^ALBlockOperationFinishedBlock)(void); | |
typedef void (^ALBlockOperationExecutionBlock)(ALBlockOperation * _Nullable weakOperation, ALBlockOperationFinishedBlock operationDidFinishBlock); | |
// [anthony, 2017-10-5] this class was made for block-based operations | |
// that need to run asychronous code in their execution block. by default, | |
// NSBlockOperations execute synchronously, but they _do not_ wait for | |
// async code within the exection block to return. this means that an | |
// operation queue can potentially discard the operation before the exection | |
// block has completely finished, making it impossible to cancel the operation | |
// if the work inside the execution block takes longer than the amount of | |
// time the operation remains in the operation queue. this class exposes | |
// a completion block in the execution handler, and this completion block | |
// MUST be called by outside classes at EVERY exit point of the execution | |
// block. the firing of the completion block will notify the operation | |
// queue that the operation has completely finished, at which point the | |
// operation queue will discard the operation. | |
// | |
@interface ALBlockOperation : NSOperation | |
// both init methods expose two objects in the execution handler: a weak | |
// reference to the operation object itself that can be used for checking | |
// if the operation was cancelled, and a completion block that MUST be | |
// called at every exit point of the execution block. | |
// | |
+ (instancetype)blockOperationWithExecutionBlock:(ALBlockOperationExecutionBlock)executionBlock; | |
- (instancetype)init NS_UNAVAILABLE; | |
- (instancetype)initWithExecutionBlock:(ALBlockOperationExecutionBlock)executionBlock; | |
@end | |
NS_ASSUME_NONNULL_END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ALBlockOperation.m | |
// | |
// Created by Anthony Lobianco on 10/5/17. | |
// | |
#import "ALBlockOperation.h" | |
typedef NS_ENUM(NSUInteger, ALBlockOperationState) { | |
ALBlockOperationStateReady = 0, // default | |
ALBlockOperationStateExecuting, | |
ALBlockOperationStateFinished | |
}; | |
@interface ALBlockOperation () | |
@property (nonatomic, readonly) ALBlockOperationExecutionBlock executionBlock; | |
// the isReady, isExecuting, and isFinished getter methods must be safe to call | |
// from other threads simultaneously. since they rely on the value of `state`, | |
// my first attempt was to synchronize calls to the state's getter and setter | |
// using a serial dispatch queue, but that would require a dispatch_sync here | |
// and we don't know what thread those methods will be called from (sometimes | |
// it's the main thread). so making the state property atomic will be good | |
// enough for our needs. | |
// | |
@property (atomic) ALBlockOperationState state; | |
@end | |
@implementation ALBlockOperation | |
#pragma mark - Initializers | |
+ (instancetype)blockOperationWithExecutionBlock:(ALBlockOperationExecutionBlock)executionBlock { | |
return [[ALBlockOperation alloc] initWithExecutionBlock:executionBlock]; | |
} | |
- (instancetype)initWithExecutionBlock:(ALBlockOperationExecutionBlock)executionBlock { | |
if ((self = [super init]) == nil) { | |
return nil; | |
} | |
_executionBlock = executionBlock; | |
return self; | |
} | |
#pragma mark - NSOperation overrides | |
- (void)start { | |
// this method will be called on an arbitrary (but appropriate) background | |
// thread by the NSOperationQueue that this operation was added to. | |
// | |
NSAssert([NSThread isMainThread] == NO, @"Operation should not be run on main thread") | |
if ([self isCancelled]) { | |
self.state = ALBlockOperationStateFinished; | |
return; | |
} | |
[self main]; | |
} | |
- (void)main { | |
NSAssert(self.executionBlock != nil, @"Operation should have an execution block"); | |
self.state = ALBlockOperationStateExecuting; | |
ALBlockOperationFinishedBlock finishedBlock = ^{ | |
self.state = ALBlockOperationStateFinished; | |
}; | |
__weak typeof(self) weakSelf = self; | |
// pass a weak reference to self in the execution block so outside classes can | |
// check if the operation was cancelled without worrying about a retain cycle. | |
// | |
self.executionBlock(weakSelf, finishedBlock); | |
} | |
- (BOOL)isReady { | |
// isReady must call through to super | |
return self.state == ALBlockOperationStateReady && [super isReady]; | |
} | |
- (BOOL)isExecuting { | |
return self.state == ALBlockOperationStateExecuting; | |
} | |
- (BOOL)isFinished { | |
return self.state == ALBlockOperationStateFinished; | |
} | |
- (BOOL)isAsynchronous { | |
// the meat of this subclass | |
return YES; | |
} | |
#pragma mark - KVO | |
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key { | |
NSMutableSet<NSString *> *keyPaths = [[super keyPathsForValuesAffectingValueForKey:key] mutableCopy]; | |
if ([self stateDoesAffectValueForKey:key]) { | |
[keyPaths addObject:NSStringFromSelector(@selector(state))]; | |
} | |
return [keyPaths copy]; | |
} | |
#pragma mark - Helper methods | |
+ (BOOL)stateDoesAffectValueForKey:(NSString *)key { | |
// the value of our internal state property directly affects these three getters | |
NSString *isReadySelString = NSStringFromSelector(@selector(isReady)); | |
NSString *isExecutingSelString = NSStringFromSelector(@selector(isExecuting)); | |
NSString *isFinishedSelString = NSStringFromSelector(@selector(isFinished)); | |
return ([key isEqualToString:isReadySelString] | |
|| [key isEqualToString:isExecutingSelString] | |
|| [key isEqualToString:isFinishedSelString]); | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment