diff --git a/Bolts.xcodeproj/project.pbxproj b/Bolts.xcodeproj/project.pbxproj index 15d5471a0..f91a4ad67 100644 --- a/Bolts.xcodeproj/project.pbxproj +++ b/Bolts.xcodeproj/project.pbxproj @@ -15,6 +15,10 @@ 1EC3017118CDAA8400D06D07 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EC3017018CDAA8400D06D07 /* AppDelegate.m */; }; 1EC3017318CDAA8400D06D07 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EC3017218CDAA8400D06D07 /* Images.xcassets */; }; 1EC3019118CDABCE00D06D07 /* AppLinkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EC3019018CDABCE00D06D07 /* AppLinkTests.m */; }; + 3D7366631A323398002E16AD /* BFTaskCancellationToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D7366611A323398002E16AD /* BFTaskCancellationToken.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3D7366641A323398002E16AD /* BFTaskCancellationToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D7366621A323398002E16AD /* BFTaskCancellationToken.m */; }; + 3D7366651A32375F002E16AD /* BFTaskCancellationToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D7366611A323398002E16AD /* BFTaskCancellationToken.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3D7366661A325686002E16AD /* BFTaskCancellationToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D7366621A323398002E16AD /* BFTaskCancellationToken.m */; }; 8103FA6819900A84000BAE3F /* BFExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA4F19900A84000BAE3F /* BFExecutor.m */; }; 8103FA6919900A84000BAE3F /* BFExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA4F19900A84000BAE3F /* BFExecutor.m */; }; 8103FA6A19900A84000BAE3F /* BFTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA5119900A84000BAE3F /* BFTask.m */; }; @@ -101,6 +105,8 @@ 1EC3017018CDAA8400D06D07 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 1EC3017218CDAA8400D06D07 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 1EC3019018CDABCE00D06D07 /* AppLinkTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppLinkTests.m; sourceTree = ""; }; + 3D7366611A323398002E16AD /* BFTaskCancellationToken.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFTaskCancellationToken.h; sourceTree = ""; }; + 3D7366621A323398002E16AD /* BFTaskCancellationToken.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFTaskCancellationToken.m; sourceTree = ""; }; 8103FA4E19900A84000BAE3F /* BFExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFExecutor.h; sourceTree = ""; }; 8103FA4F19900A84000BAE3F /* BFExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFExecutor.m; sourceTree = ""; }; 8103FA5019900A84000BAE3F /* BFTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFTask.h; sourceTree = ""; }; @@ -230,6 +236,8 @@ 8103FA5519900A84000BAE3F /* Bolts.m */, 8103FA5019900A84000BAE3F /* BFTask.h */, 8103FA5119900A84000BAE3F /* BFTask.m */, + 3D7366611A323398002E16AD /* BFTaskCancellationToken.h */, + 3D7366621A323398002E16AD /* BFTaskCancellationToken.m */, 8103FA5219900A84000BAE3F /* BFTaskCompletionSource.h */, 8103FA5319900A84000BAE3F /* BFTaskCompletionSource.m */, 8103FA4E19900A84000BAE3F /* BFExecutor.h */, @@ -366,6 +374,7 @@ 81D0EE9219AFAA6F0000AE75 /* BFMeasurementEvent.h in Headers */, 81D0EE9319AFAA6F0000AE75 /* BFURL.h in Headers */, 81D0EE8419AFAA100000AE75 /* Bolts.h in Headers */, + 3D7366631A323398002E16AD /* BFTaskCancellationToken.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -375,6 +384,7 @@ files = ( 81D0EE8519AFAA190000AE75 /* BFTask.h in Headers */, 81D0EE8819AFAA240000AE75 /* BFExecutor.h in Headers */, + 3D7366651A32375F002E16AD /* BFTaskCancellationToken.h in Headers */, 81D0EE8A19AFAA2C0000AE75 /* BFTaskCompletionSource.h in Headers */, 81D0EE8219AFAA060000AE75 /* BoltsVersion.h in Headers */, 81D0EE8319AFAA0E0000AE75 /* Bolts.h in Headers */, @@ -571,6 +581,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3D7366641A323398002E16AD /* BFTaskCancellationToken.m in Sources */, 8103FA7819900A84000BAE3F /* BFAppLinkTarget.m in Sources */, 8103FA6C19900A84000BAE3F /* BFTaskCompletionSource.m in Sources */, 8103FA7619900A84000BAE3F /* BFAppLinkReturnToRefererView.m in Sources */, @@ -594,6 +605,7 @@ 8103FA6B19900A84000BAE3F /* BFTask.m in Sources */, 8103FA6F19900A84000BAE3F /* Bolts.m in Sources */, 8103FA6919900A84000BAE3F /* BFExecutor.m in Sources */, + 3D7366661A325686002E16AD /* BFTaskCancellationToken.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Bolts.xcodeproj/project.xcworkspace/xcshareddata/Bolts.xccheckout b/Bolts.xcodeproj/project.xcworkspace/xcshareddata/Bolts.xccheckout index 953d5db46..d59e579ee 100644 --- a/Bolts.xcodeproj/project.xcworkspace/xcshareddata/Bolts.xccheckout +++ b/Bolts.xcodeproj/project.xcworkspace/xcshareddata/Bolts.xccheckout @@ -11,17 +11,17 @@ IDESourceControlProjectOriginsDictionary 61C4B9E3B61282127C102C87B46B8CDE985974AE - ssh://github.com/BoltsFramework/Bolts-iOS.git + github.com:BoltsFramework/Bolts-iOS.git IDESourceControlProjectPath - Bolts.xcodeproj/project.xcworkspace + Bolts.xcodeproj IDESourceControlProjectRelativeInstallPathDictionary 61C4B9E3B61282127C102C87B46B8CDE985974AE ../.. IDESourceControlProjectURL - ssh://github.com/BoltsFramework/Bolts-iOS.git + github.com:BoltsFramework/Bolts-iOS.git IDESourceControlProjectVersion 111 IDESourceControlProjectWCCIdentifier diff --git a/Bolts/Common/BFTask.h b/Bolts/Common/BFTask.h index 2ac84d601..3ce3a80d3 100644 --- a/Bolts/Common/BFTask.h +++ b/Bolts/Common/BFTask.h @@ -12,6 +12,7 @@ @class BFExecutor; @class BFTask; +@class BFTaskCancellationToken; /*! A block that can act as a continuation for a task. @@ -155,6 +156,73 @@ typedef id(^BFContinuationBlock)(BFTask *task); - (instancetype)continueWithExecutor:(BFExecutor *)executor withSuccessBlock:(BFContinuationBlock)block; +/*! + Enques the given block to be run once the task is complete + @param executor A BFExecutor responsible for determining how the + continuation block will be run. + @param cancellationToken A BFTaskCancellationToken which allows + the task to be cancelled before the continuation block is executed + @param block The block to be run once this task is complete. + @returns A task that will be completed after block has run. + If block returns a BFTask, then the task returned from + this method will not be completed until that task is completed. + If the cancellationToken is cancelled before the block is executed + then the BFTask returned will have isCancelled true + */ +- (instancetype)continueWithExecutor:(BFExecutor *)executor + withCancellationToken:(BFTaskCancellationToken *)cancellationToken + withBlock:(BFContinuationBlock)block; +/*! + Identical to continueWithExecutor:withCancellationToken:withBlock:, + except that it uses the default execution strategy + @param cancellationToken A BFTaskCancellationToken which allows + the task to be cancelled before the continuation block is executed + @param block The block to be run once this task is complete. + @returns A task that will be completed after block has run. + If block returns a BFTask, then the task returned from + this method will not be completed until that task is completed. + If the cancellationToken is cancelled before the block is executed + then the BFTask returned will have isCancelled true + */ +- (instancetype)continueWithCancellationToken:(BFTaskCancellationToken *)cancellationToken + withBlock:(BFContinuationBlock)block; + +/*! + Identical to continueWithExecutor:withCancellationToken:withBlock:, + except that the cancellation token is checked and the block executed + only if this task did not produce a cancellation, error or exception. + @param executor A BFExecutor responsible for determining how the + continuation block will be run. + @param cancellationToken A BFTaskCancellationToken which allows + the task to be cancelled before the continuation block is executed + @param block The block to be run once this task is complete. + @returns A task that will be completed after block has run. + If block returns a BFTask, then the task returned from + this method will not be completed until that task is completed. + If the cancellationToken is cancelled before the block is executed + and this task has not produced a cancellation, error or exception then + the BFTask returned will have isCancelled true. + */ +- (instancetype)continueWithExecutor:(BFExecutor *)executor + withCancellationToken:(BFTaskCancellationToken *)token + withSuccessBlock:(BFContinuationBlock)block; + +/*! + Identical to continueWithExecutor:withCancellationToken:withSuccessBlock:, + except that it uses the default execution strategy + @param cancellationToken A BFTaskCancellationToken which allows + the task to be cancelled before the continuation block is executed + @param block The block to be run once this task is complete. + @returns A task that will be completed after block has run. + If block returns a BFTask, then the task returned from + this method will not be completed until that task is completed. + If the cancellationToken is cancelled before the block is executed + and this task has not produced a cancellation, error or exception then + the BFTask returned will have isCancelled true. + */ +- (instancetype)continueWithCancellationToken:(BFTaskCancellationToken *)cancellationToken + withSuccessBlock:(BFContinuationBlock)block; + /*! Waits until this operation is completed. This method is inefficient and consumes a thread resource while diff --git a/Bolts/Common/BFTask.m b/Bolts/Common/BFTask.m index 157f06255..21b77a0da 100644 --- a/Bolts/Common/BFTask.m +++ b/Bolts/Common/BFTask.m @@ -341,6 +341,44 @@ - (instancetype)continueWithSuccessBlock:(BFContinuationBlock)block { return [self continueWithExecutor:[BFExecutor defaultExecutor] withSuccessBlock:block]; } +- (instancetype)continueWithExecutor:(BFExecutor *)executor + withCancellationToken:(BFTaskCancellationToken *)token + withBlock:(BFContinuationBlock)block { + return [self continueWithExecutor:executor withBlock:^id(BFTask *task) { + if (token.isCancelled) { + return [BFTask cancelledTask]; + } else { + return block(task); + } + }]; +} + +- (instancetype)continueWithCancellationToken:(BFTaskCancellationToken *)token + withBlock:(BFContinuationBlock)block { + return [self continueWithExecutor:[BFExecutor defaultExecutor] + withCancellationToken:token + withBlock:block]; +} + +- (instancetype)continueWithExecutor:(BFExecutor *)executor + withCancellationToken:(BFTaskCancellationToken *)token + withSuccessBlock:(BFContinuationBlock)block { + return [self continueWithExecutor:executor withSuccessBlock:^id(BFTask *task) { + if (token.isCancelled) { + return [BFTask cancelledTask]; + } else { + return block(task); + } + }]; +} + +- (instancetype)continueWithCancellationToken:(BFTaskCancellationToken *)token + withSuccessBlock:(BFContinuationBlock)block { + return [self continueWithExecutor:[BFExecutor defaultExecutor] + withCancellationToken:token + withSuccessBlock:block]; +} + #pragma mark - Syncing Task (Avoid it) - (void)warnOperationOnMainThread { diff --git a/Bolts/Common/BFTaskCancellationToken.h b/Bolts/Common/BFTaskCancellationToken.h new file mode 100644 index 000000000..d2e1d2a4d --- /dev/null +++ b/Bolts/Common/BFTaskCancellationToken.h @@ -0,0 +1,16 @@ +// +// BFTaskCancellationToken.h +// Bolts +// +// Created by Daniel Hammond on 12/5/14. +// Copyright (c) 2014 Parse Inc. All rights reserved. +// + +#import + +@interface BFTaskCancellationToken : NSObject + +@property (nonatomic, assign, readonly, getter=isCancelled) BOOL cancelled; +- (void)cancel; + +@end diff --git a/Bolts/Common/BFTaskCancellationToken.m b/Bolts/Common/BFTaskCancellationToken.m new file mode 100644 index 000000000..208f0084b --- /dev/null +++ b/Bolts/Common/BFTaskCancellationToken.m @@ -0,0 +1,39 @@ +// +// BFTaskCancellationToken.m +// Bolts +// +// Created by Daniel Hammond on 12/5/14. +// Copyright (c) 2014 Parse Inc. All rights reserved. +// + +#import "BFTaskCancellationToken.h" + +@interface BFTaskCancellationToken () + +@property (atomic, assign, readwrite, getter=isCancelled) BOOL cancelled; +@property (nonatomic, retain) NSObject *lock; + +@end + +@implementation BFTaskCancellationToken + +- (instancetype)init +{ + self = [super init]; + _lock = [[NSObject alloc] init]; + return self; +} + +- (BOOL)isCancelled { + @synchronized (self.lock) { + return _cancelled; + } +} + +- (void)cancel { + @synchronized (self.lock) { + self.cancelled = YES; + } +} + +@end diff --git a/Bolts/Common/Bolts.h b/Bolts/Common/Bolts.h index e22bdd541..effd8ac23 100644 --- a/Bolts/Common/Bolts.h +++ b/Bolts/Common/Bolts.h @@ -12,6 +12,7 @@ #import #import #import +#import #if TARGET_OS_IPHONE #import diff --git a/BoltsTests/TaskTests.m b/BoltsTests/TaskTests.m index 15b0e9387..66a3c0d8d 100644 --- a/BoltsTests/TaskTests.m +++ b/BoltsTests/TaskTests.m @@ -235,6 +235,80 @@ - (void)testCancellation { XCTAssertTrue(task.isCancelled); } +- (void)testCancellationToken { + BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init]; + [token cancel]; + BFTask *task = [BFTask taskWithDelay:100]; + task = [task continueWithExecutor:[BFExecutor defaultExecutor] + withCancellationToken:token + withBlock:^id(BFTask *task) { + XCTFail(@"This callback should be skipped"); + return nil; + }]; + [task waitUntilFinished]; + XCTAssertTrue(task.cancelled); +} + +- (void)testCancellationTokenNotCancelled { + BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init]; + BFTask *task = [BFTask taskWithResult:@"foo"]; + __block BOOL blockExecuted = NO; + [[task continueWithExecutor:[BFExecutor defaultExecutor] + withCancellationToken:token + withBlock:^id(BFTask *task) { + XCTAssertEqualObjects(@"foo", task.result); + blockExecuted = YES; + return task; + }] waitUntilFinished]; + XCTAssertTrue(blockExecuted); +} + +- (void)testCancellationTokenSuccessBlock { + BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init]; + [token cancel]; + BFTask *task = [BFTask taskWithDelay:100]; + task = [task continueWithExecutor:[BFExecutor defaultExecutor] + withCancellationToken:token + withSuccessBlock:^id(BFTask *task) { + XCTFail(@"This callback should be skipped"); + return nil; + }]; + [task waitUntilFinished]; + XCTAssertTrue(task.cancelled); +} + +- (void)testCancellationTokenSuccessBlockNotCancelled { + BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init]; + BFTask *task = [BFTask taskWithResult:@"foo"]; + __block BOOL blockExecuted = NO; + [[task continueWithExecutor:[BFExecutor defaultExecutor] + withCancellationToken:token + withBlock:^id(BFTask *task) { + XCTAssertEqualObjects(@"foo", task.result); + blockExecuted = YES; + return task; + }] waitUntilFinished]; + XCTAssertTrue(blockExecuted); +} + +- (void)testCancellationTokenSuccessBlockError { + BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init]; + [token cancel]; + NSError *error = [NSError errorWithDomain:@"BoltsTests" code:35 userInfo:nil]; + BFTask *task = [BFTask taskWithError:error]; + task = [[task continueWithExecutor:[BFExecutor defaultExecutor] + withCancellationToken:token + withSuccessBlock:^id(BFTask *task) { + XCTFail(@"This callback should be skipped"); + return nil; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.isCancelled); + XCTAssertEqualObjects(error, task.error); + return nil; + }]; + [task waitUntilFinished]; +} + - (void)testTaskForCompletionOfAllTasksSuccess { NSMutableArray *tasks = [NSMutableArray array]; diff --git a/Readme.md b/Readme.md index 9757e510a..a090ed96f 100644 --- a/Readme.md +++ b/Readme.md @@ -476,29 +476,24 @@ For common cases, such as dispatching on the main thread, we have provided defau ## Task Cancellation -It's generally bad design to keep track of the `BFTaskCompletionSource` for cancellation. A better model is to create a "cancellation token" at the top level, and pass that to each async function that you want to be part of the same "cancelable operation". Then, in your continuation blocks, you can check whether the cancellation token has been cancelled and bail out early by returning a `[BFTask cancelledTask]`. For example: +If you need to be able to cancel a `BFTask` the `continueWithCancellationToken:withBlock` and `continueWithCancellationToken:withSuccessBlock` method allow you to pass in a `BFTaskCancellationToken` object which is checked before executing the continuation block. -```objective-c -- (void)doSomethingComplicatedAsync:(MYCancellationToken *)cancellationToken { - [[self doSomethingAsync:cancellationToken] continueWithBlock:^{ - if (cancellationToken.isCancelled) { - return [BFTask cancelledTask]; - } - // Do something that takes a while. - return result; - }]; -} - -// Somewhere else. -MYCancellationToken *cancellationToken = [[MYCancellationToken alloc] init]; -[obj doSomethingComplicatedAsync:cancellationToken]; - -// When you get bored... -[cancellationToken cancel]; -``` +````objective-c +PFQuery *query = [PFQuery queryWithClassName:@"Images"]; +BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init]; -**Note:** The cancellation token implementation should be thread-safe. -We are likely to add some concept like this to Bolts at some point in the future. +[[[self findAsync:query] continueWithCancellationToken:token withBlock:^id(BFTask *task) { + // This won't be executed if -cancel is called on token before the query has returned + return [self processImages:task.results]; +}] continueWithBlock:^id(BFTask *task) { + if (task.isCancelled) { + // Handle cancellation if needed + } else { + // Update UI etc... + } + return nil; +}]; +```` # App Links