Skip to content

Instantly share code, notes, and snippets.

@lobianco
Created June 20, 2018 19:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lobianco/80ae290c8063d45b65e54b8c418e56aa to your computer and use it in GitHub Desktop.
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.
//
// 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
//
// 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