From a3c10cd8eeabd6a09ef738eccdcfb2723ff018fd Mon Sep 17 00:00:00 2001 From: Anthony Shoumikhin Date: Wed, 16 May 2018 09:12:46 -0700 Subject: [PATCH] Draft progress prototyping --- Promises.xcodeproj/project.pbxproj | 28 ++++- PromisesObjC.podspec | 4 +- PromisesSwift.podspec | 4 +- Sources/FBLPromises/FBLPromise+Async.m | 39 ++++++- Sources/FBLPromises/FBLPromise+Catch.m | 5 +- Sources/FBLPromises/FBLPromise+Recover.m | 2 +- Sources/FBLPromises/FBLPromise+Then.m | 38 ++++++- Sources/FBLPromises/FBLPromise.m | 68 +++++++++++- Sources/FBLPromises/FBLPromiseProgress.m | 78 +++++++++++++ .../FBLPromises/include/FBLPromise+Async.h | 16 ++- Sources/FBLPromises/include/FBLPromise+Then.h | 16 ++- Sources/FBLPromises/include/FBLPromise.h | 4 + Sources/FBLPromises/include/FBLPromiseError.h | 11 +- .../FBLPromises/include/FBLPromisePrivate.h | 8 ++ .../FBLPromises/include/FBLPromiseProgress.h | 28 +++++ .../include/FBLPromiseProgressPrivate.h | 31 ++++++ .../FBLPromises/include/framework.modulemap | 1 + Sources/FBLPromises/include/module.modulemap | 1 + Sources/Promises/Promise+All.swift | 4 +- Sources/Promises/Promise+Any.swift | 6 +- Sources/Promises/Promise+Async.swift | 24 +++- Sources/Promises/Promise+Then.swift | 104 ++++++++++++------ Sources/Promises/Promise.swift | 6 + .../FBLPromise+ProgressTests.m | 63 +++++++++++ .../PromisesTests/Promise+ProgressTests.swift | 51 +++++++++ 25 files changed, 576 insertions(+), 64 deletions(-) create mode 100644 Sources/FBLPromises/FBLPromiseProgress.m create mode 100644 Sources/FBLPromises/include/FBLPromiseProgress.h create mode 100644 Sources/FBLPromises/include/FBLPromiseProgressPrivate.h create mode 100644 Tests/FBLPromisesTests/FBLPromise+ProgressTests.m create mode 100644 Tests/PromisesTests/Promise+ProgressTests.swift diff --git a/Promises.xcodeproj/project.pbxproj b/Promises.xcodeproj/project.pbxproj index 86fdc3b..5df7e7b 100644 --- a/Promises.xcodeproj/project.pbxproj +++ b/Promises.xcodeproj/project.pbxproj @@ -104,6 +104,11 @@ 035D15F520915B550089EF3D /* FBLPromise+AwaitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 035D15F320915B490089EF3D /* FBLPromise+AwaitTests.m */; }; 035D15F8209220120089EF3D /* FBLPromise+Delay.m in Sources */ = {isa = PBXBuildFile; fileRef = 035D15EE20911AB70089EF3D /* FBLPromise+Delay.m */; }; 035D15F9209220150089EF3D /* FBLPromise+Await.m in Sources */ = {isa = PBXBuildFile; fileRef = 035D15E6208EF8750089EF3D /* FBLPromise+Await.m */; }; + 038E868E20A81C7800AD1FF7 /* FBLPromise+ProgressTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 034D93AA20A808A400289777 /* FBLPromise+ProgressTests.m */; }; + 038E869120A88C0E00AD1FF7 /* FBLPromiseProgress.h in Headers */ = {isa = PBXBuildFile; fileRef = 038E868F20A88C0100AD1FF7 /* FBLPromiseProgress.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 038E869220A88C1A00AD1FF7 /* FBLPromiseProgressPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 038E869020A88C0200AD1FF7 /* FBLPromiseProgressPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 038E869420A88C2E00AD1FF7 /* FBLPromiseProgress.m in Sources */ = {isa = PBXBuildFile; fileRef = 038E869320A88C2E00AD1FF7 /* FBLPromiseProgress.m */; }; + 038E869720A9241300AD1FF7 /* Promise+ProgressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038E869520A9240D00AD1FF7 /* Promise+ProgressTests.swift */; }; 0391947B2095B73200C40218 /* Promise+DelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039194792095B72300C40218 /* Promise+DelayTests.swift */; }; 0391947E2095B9C600C40218 /* FBLPromise+DelayTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0391947C2095B9BE00C40218 /* FBLPromise+DelayTests.m */; }; OBJ_134 /* PromisesTestHelpers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "Promises::PromisesTestHelpers::Product" /* PromisesTestHelpers.framework */; }; @@ -387,6 +392,7 @@ 03326C312084642000872827 /* FBLPromise+Reduce.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "FBLPromise+Reduce.m"; sourceTree = ""; }; 03326C342084643600872827 /* FBLPromise+Reduce.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FBLPromise+Reduce.h"; sourceTree = ""; }; 03326C36208464C100872827 /* FBLPromise+ReduceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "FBLPromise+ReduceTests.m"; sourceTree = ""; }; + 034D93AA20A808A400289777 /* FBLPromise+ProgressTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "FBLPromise+ProgressTests.m"; sourceTree = ""; }; 035D15DF208EDD460089EF3D /* Promise+Await.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Await.swift"; sourceTree = ""; }; 035D15E2208EE1F10089EF3D /* Promise+AwaitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+AwaitTests.swift"; sourceTree = ""; }; 035D15E6208EF8750089EF3D /* FBLPromise+Await.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "FBLPromise+Await.m"; sourceTree = ""; }; @@ -395,6 +401,10 @@ 035D15EE20911AB70089EF3D /* FBLPromise+Delay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "FBLPromise+Delay.m"; sourceTree = ""; }; 035D15F120911ACA0089EF3D /* FBLPromise+Delay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FBLPromise+Delay.h"; sourceTree = ""; }; 035D15F320915B490089EF3D /* FBLPromise+AwaitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "FBLPromise+AwaitTests.m"; sourceTree = ""; }; + 038E868F20A88C0100AD1FF7 /* FBLPromiseProgress.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBLPromiseProgress.h; sourceTree = ""; }; + 038E869020A88C0200AD1FF7 /* FBLPromiseProgressPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBLPromiseProgressPrivate.h; sourceTree = ""; }; + 038E869320A88C2E00AD1FF7 /* FBLPromiseProgress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBLPromiseProgress.m; sourceTree = ""; }; + 038E869520A9240D00AD1FF7 /* Promise+ProgressTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+ProgressTests.swift"; sourceTree = ""; }; 039194792095B72300C40218 /* Promise+DelayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+DelayTests.swift"; sourceTree = ""; }; 0391947C2095B9BE00C40218 /* FBLPromise+DelayTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "FBLPromise+DelayTests.m"; sourceTree = ""; }; "Promises::FBLPromises::Product" /* FBLPromises.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FBLPromises.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -591,6 +601,7 @@ 03204070204547D300D2D16C /* FBLPromise+Validate.m */, 03204050204547D300D2D16C /* FBLPromise+Wrap.m */, 03204066204547D300D2D16C /* FBLPromiseError.m */, + 038E869320A88C2E00AD1FF7 /* FBLPromiseProgress.m */, 03204051204547D300D2D16C /* include */, ); path = FBLPromises; @@ -618,6 +629,8 @@ 03204061204547D300D2D16C /* FBLPromise+Wrap.h */, 0320405D204547D300D2D16C /* FBLPromiseError.h */, 0320405A204547D300D2D16C /* FBLPromisePrivate.h */, + 038E868F20A88C0100AD1FF7 /* FBLPromiseProgress.h */, + 038E869020A88C0200AD1FF7 /* FBLPromiseProgressPrivate.h */, 03204059204547D300D2D16C /* FBLPromises.h */, 0320405F204547D300D2D16C /* framework.modulemap */, ); @@ -640,6 +653,7 @@ 03204073204547D300D2D16C /* PromisesTests */ = { isa = PBXGroup; children = ( + 038E869520A9240D00AD1FF7 /* Promise+ProgressTests.swift */, 03204074204547D300D2D16C /* Promise+AllTests.swift */, 0320407A204547D300D2D16C /* Promise+AlwaysTests.swift */, 03204080204547D400D2D16C /* Promise+AnyTests.swift */, @@ -688,6 +702,7 @@ 03204088204547D400D2D16C /* FBLPromisesTests */ = { isa = PBXGroup; children = ( + 034D93AA20A808A400289777 /* FBLPromise+ProgressTests.m */, 03204090204547D400D2D16C /* FBLPromise+AllTests.m */, 0320408A204547D400D2D16C /* FBLPromise+AlwaysTests.m */, 03204089204547D400D2D16C /* FBLPromise+AnyTests.m */, @@ -750,6 +765,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 038E869120A88C0E00AD1FF7 /* FBLPromiseProgress.h in Headers */, 032B80FF204549080097BF12 /* FBLPromise+Race.h in Headers */, 035D15F220911AD30089EF3D /* FBLPromise+Delay.h in Headers */, 032B8109204549080097BF12 /* FBLPromise+Any.h in Headers */, @@ -757,6 +773,7 @@ 032B8100204549080097BF12 /* FBLPromise+Async.h in Headers */, 032B80FC204549080097BF12 /* FBLPromise.h in Headers */, 032B8106204549080097BF12 /* FBLPromise+Then.h in Headers */, + 038E869220A88C1A00AD1FF7 /* FBLPromiseProgressPrivate.h in Headers */, 032B8108204549080097BF12 /* FBLPromise+Validate.h in Headers */, 032B810B204549080097BF12 /* FBLPromisePrivate.h in Headers */, 032B80FE204549080097BF12 /* FBLPromise+Always.h in Headers */, @@ -1026,6 +1043,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( + 038E869720A9241300AD1FF7 /* Promise+ProgressTests.swift in Sources */, 032B813B204549700097BF12 /* Promise+DoTests.swift in Sources */, 03326C3020841E4D00872827 /* Promise+ReduceTests.swift in Sources */, 032B8141204549700097BF12 /* Promise+AnyTests.swift in Sources */, @@ -1058,6 +1076,7 @@ 032B812B204549590097BF12 /* FBLPromise+DoTests.m in Sources */, 032B812D204549590097BF12 /* FBLPromise+WrapTests.m in Sources */, 032B8127204549590097BF12 /* FBLPromise+AlwaysTests.m in Sources */, + 038E868E20A81C7800AD1FF7 /* FBLPromise+ProgressTests.m in Sources */, 035D15F520915B550089EF3D /* FBLPromise+AwaitTests.m in Sources */, 032B8131204549590097BF12 /* FBLPromise+AnyTests.m in Sources */, 032B812E204549590097BF12 /* FBLPromise+ThenTests.m in Sources */, @@ -1139,6 +1158,7 @@ 035D15F9209220150089EF3D /* FBLPromise+Await.m in Sources */, 035D15F8209220120089EF3D /* FBLPromise+Delay.m in Sources */, 032B80EE204549000097BF12 /* FBLPromise+All.m in Sources */, + 038E869420A88C2E00AD1FF7 /* FBLPromiseProgress.m in Sources */, 032B80F1204549000097BF12 /* FBLPromise+Async.m in Sources */, 032B80F4204549000097BF12 /* FBLPromise+Recover.m in Sources */, 032B80F5204549000097BF12 /* FBLPromise+Wrap.m in Sources */, @@ -1739,8 +1759,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.10; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; @@ -1785,8 +1805,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.10; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/PromisesObjC.podspec b/PromisesObjC.podspec index 1f87941..18fe2a1 100644 --- a/PromisesObjC.podspec +++ b/PromisesObjC.podspec @@ -12,8 +12,8 @@ Pod::Spec.new do |s| Objective-C to facilitate writing asynchronous code. DESC - s.ios.deployment_target = '8.0' - s.osx.deployment_target = '10.10' + s.ios.deployment_target = '9.0' + s.osx.deployment_target = '10.11' s.tvos.deployment_target = '9.0' s.watchos.deployment_target = '2.0' diff --git a/PromisesSwift.podspec b/PromisesSwift.podspec index cf98cb0..699e72a 100644 --- a/PromisesSwift.podspec +++ b/PromisesSwift.podspec @@ -12,8 +12,8 @@ Pod::Spec.new do |s| Swift to facilitate writing asynchronous code. DESC - s.ios.deployment_target = '8.0' - s.osx.deployment_target = '10.10' + s.ios.deployment_target = '9.0' + s.osx.deployment_target = '10.11' s.tvos.deployment_target = '9.0' s.watchos.deployment_target = '2.0' s.swift_version = '4.0' diff --git a/Sources/FBLPromises/FBLPromise+Async.m b/Sources/FBLPromises/FBLPromise+Async.m index 249158c..bf35cfc 100644 --- a/Sources/FBLPromises/FBLPromise+Async.m +++ b/Sources/FBLPromises/FBLPromise+Async.m @@ -21,14 +21,36 @@ @implementation FBLPromise (AsyncAdditions) + (instancetype)async:(FBLPromiseAsyncWorkBlock)work { - return [self onQueue:self.defaultDispatchQueue async:work]; + return [self onQueue:self.defaultDispatchQueue + progressUnits:1 + async:^(FBLPromiseFulfillBlock fulfill, FBLPromiseRejectBlock reject, + NSProgress *__unused _) { + work(fulfill, reject); + }]; } + (instancetype)onQueue:(dispatch_queue_t)queue async:(FBLPromiseAsyncWorkBlock)work { + return [self onQueue:queue + progressUnits:1 + async:^(FBLPromiseFulfillBlock fulfill, FBLPromiseRejectBlock reject, + NSProgress *__unused _) { + work(fulfill, reject); + }]; +} + ++ (instancetype)progressUnits:(int64_t)totalUnitCount async:(FBLPromiseAsyncProgressWorkBlock)work { + return [self onQueue:self.defaultDispatchQueue progressUnits:totalUnitCount async:work]; +} + ++ (instancetype)onQueue:(dispatch_queue_t)queue + progressUnits:(int64_t)totalUnitCount + async:(FBLPromiseAsyncProgressWorkBlock)work { NSParameterAssert(queue); + NSParameterAssert(totalUnitCount > 0); NSParameterAssert(work); FBLPromise *promise = [[FBLPromise alloc] initPending]; + promise.progress.totalUnitCount = totalUnitCount; dispatch_group_async(FBLPromise.dispatchGroup, queue, ^{ work( ^(id __nullable value) { @@ -46,7 +68,8 @@ }, ^(NSError *error) { [promise reject:error]; - }); + }, + promise.progress); }); return promise; } @@ -67,4 +90,16 @@ }; } ++ (FBLPromise* (^)(int64_t, FBLPromiseAsyncProgressWorkBlock))asyncProgress { + return ^(int64_t totalUnitCount, FBLPromiseAsyncProgressWorkBlock work) { + return [self progressUnits:totalUnitCount async:work]; + }; +} + ++ (FBLPromise* (^)(dispatch_queue_t, int64_t, FBLPromiseAsyncProgressWorkBlock))asyncProgressOn { + return ^(dispatch_queue_t queue, int64_t totalUnitCount, FBLPromiseAsyncProgressWorkBlock work) { + return [self onQueue:queue progressUnits:totalUnitCount async:work]; + }; +} + @end diff --git a/Sources/FBLPromises/FBLPromise+Catch.m b/Sources/FBLPromises/FBLPromise+Catch.m index 25e8ce6..4de328e 100644 --- a/Sources/FBLPromises/FBLPromise+Catch.m +++ b/Sources/FBLPromises/FBLPromise+Catch.m @@ -29,9 +29,12 @@ NSParameterAssert(reject); return [self chainOnQueue:queue + progressUnits:1 chainedFulfill:nil chainedReject:^id(NSError *error) { - reject(error); + if (!FBLPromiseErrorIsCancelled(error)) { + reject(error); + } return error; }]; } diff --git a/Sources/FBLPromises/FBLPromise+Recover.m b/Sources/FBLPromises/FBLPromise+Recover.m index 0c9326a..09309ab 100644 --- a/Sources/FBLPromises/FBLPromise+Recover.m +++ b/Sources/FBLPromises/FBLPromise+Recover.m @@ -31,7 +31,7 @@ return [self chainOnQueue:queue chainedFulfill:nil chainedReject:^id(NSError *error) { - return recovery(error); + return FBLPromiseErrorIsCancelled(error) ? error : recovery(error); }]; } diff --git a/Sources/FBLPromises/FBLPromise+Then.m b/Sources/FBLPromises/FBLPromise+Then.m index ab03bd1..d5a02ca 100644 --- a/Sources/FBLPromises/FBLPromise+Then.m +++ b/Sources/FBLPromises/FBLPromise+Then.m @@ -21,14 +21,36 @@ @implementation FBLPromise (ThenAdditions) - (FBLPromise *)then:(FBLPromiseThenWorkBlock)work { - return [self onQueue:FBLPromise.defaultDispatchQueue then:work]; + return [self onQueue:FBLPromise.defaultDispatchQueue + progressUnits:1 + then:^id(id value, NSProgress *__unused _) { + return work(value); + }]; } - (FBLPromise *)onQueue:(dispatch_queue_t)queue then:(FBLPromiseThenWorkBlock)work { + return [self onQueue:queue + progressUnits:1 + then:^id(id value, NSProgress *__unused _) { + return work(value); + }]; +} + +- (FBLPromise *)progressUnits:(int64_t)totalUnitCount then:(FBLPromiseThenProgressWorkBlock)work { + return [self onQueue:FBLPromise.defaultDispatchQueue progressUnits:totalUnitCount then:work]; +} + +- (FBLPromise *)onQueue:(dispatch_queue_t)queue + progressUnits:(int64_t)totalUnitCount + then:(FBLPromiseThenProgressWorkBlock)work { NSParameterAssert(queue); + NSParameterAssert(totalUnitCount > 0); NSParameterAssert(work); - return [self chainOnQueue:queue chainedFulfill:work chainedReject:nil]; + return [self chainOnQueue:queue + progressUnits:totalUnitCount + chainedFulfill:work + chainedReject:nil]; } @end @@ -47,4 +69,16 @@ }; } +- (FBLPromise* (^)(int64_t, FBLPromiseThenProgressWorkBlock))thenProgress { + return ^(int64_t totalUnitCount, FBLPromiseThenProgressWorkBlock work) { + return [self progressUnits:totalUnitCount then:work]; + }; +} + +- (FBLPromise* (^)(dispatch_queue_t, int64_t, FBLPromiseThenProgressWorkBlock))thenProgressOn { + return ^(dispatch_queue_t queue, int64_t totalUnitCount, FBLPromiseThenProgressWorkBlock work) { + return [self onQueue:queue progressUnits:totalUnitCount then:work]; + }; +} + @end diff --git a/Sources/FBLPromises/FBLPromise.m b/Sources/FBLPromises/FBLPromise.m index a0f48a6..97f9c2a 100644 --- a/Sources/FBLPromises/FBLPromise.m +++ b/Sources/FBLPromises/FBLPromise.m @@ -15,6 +15,7 @@ */ #import "FBLPromisePrivate.h" +#import "FBLPromiseProgressPrivate.h" /** All states a promise can be in. */ typedef NS_ENUM(NSInteger, FBLPromiseState) { @@ -47,6 +48,8 @@ static dispatch_queue_t gFBLPromiseDefaultDispatchQueue; NSError *__nullable _error; /** List of observers to notify when the promise gets resolved. */ NSMutableArray *_observers; + + FBLPromiseProgress *_progress; } + (void)initialize { @@ -83,6 +86,7 @@ static dispatch_queue_t gFBLPromiseDefaultDispatchQueue; } else { @synchronized(self) { if (_state == FBLPromiseStatePending) { + _progress.completedUnitCount = _progress.totalUnitCount; _state = FBLPromiseStateFulfilled; _value = value; _pendingObjects = nil; @@ -117,6 +121,10 @@ static dispatch_queue_t gFBLPromiseDefaultDispatchQueue; } } +- (void)cancel { + [_progress cancel]; +} + #pragma mark - NSObject - (NSString *)description { @@ -133,8 +141,18 @@ static dispatch_queue_t gFBLPromiseDefaultDispatchQueue; #pragma mark - Private -- (instancetype)initPending { +- (instancetype)initPrivate { self = [super init]; + if (self) { + _progress = [[FBLPromiseProgress alloc] initWithParent:nil userInfo:nil]; + _progress.promise = self; + _progress.totalUnitCount = 1; + } + return self; +} + +- (instancetype)initPending { + self = [self initPrivate]; if (self) { dispatch_group_enter(FBLPromise.dispatchGroup); } @@ -142,7 +160,7 @@ static dispatch_queue_t gFBLPromiseDefaultDispatchQueue; } - (instancetype)initWithResolution:(nullable id)resolution { - self = [super init]; + self = [self initPrivate]; if (self) { if ([resolution isKindOfClass:[NSError class]]) { _state = FBLPromiseStateRejected; @@ -151,6 +169,7 @@ static dispatch_queue_t gFBLPromiseDefaultDispatchQueue; _state = FBLPromiseStateFulfilled; _value = resolution; } + _progress.completedUnitCount = _progress.totalUnitCount; } return self; } @@ -253,6 +272,49 @@ static dispatch_queue_t gFBLPromiseDefaultDispatchQueue; NSParameterAssert(queue); FBLPromise *promise = [[FBLPromise alloc] initPending]; + __auto_type resolver = ^(id __nullable value) { + if ([value isKindOfClass:[FBLPromise class]]) { + [(FBLPromise *)value observeOnQueue:queue + fulfill:^(id __nullable value) { + [promise fulfill:value]; + } + reject:^(NSError *error) { + [promise reject:error]; + }]; + } else { + [promise fulfill:value]; + } + }; + [self observeOnQueue:queue + fulfill:^(id __nullable value) { + value = chainedFulfill ? chainedFulfill(value) : value; + resolver(value); + } + reject:^(NSError *error) { + id value = chainedReject ? chainedReject(error) : error; + resolver(value); + }]; + return promise; +} + +- (FBLPromise *)chainOnQueue:(dispatch_queue_t)queue + progressUnits:(int64_t)totalUnitCount + chainedFulfill:(FBLPromiseChainedFulfillProgressBlock)chainedFulfill + chainedReject:(FBLPromiseChainedRejectBlock)chainedReject { + NSParameterAssert(queue); + NSParameterAssert(totalUnitCount > 0); + + FBLPromise *promise = [[FBLPromise alloc] initPending]; + promise.progress.totalUnitCount = _progress.totalUnitCount + totalUnitCount; + + FBLPromiseProgress *syncProgress = [[FBLPromiseProgress alloc] initWithParent:nil userInfo:nil]; + [_progress addSyncProgress:syncProgress]; + [promise.progress addChild:syncProgress withPendingUnitCount:syncProgress.totalUnitCount]; + + FBLPromiseProgress *progress = [[FBLPromiseProgress alloc] initWithParent:nil userInfo:nil]; + progress.totalUnitCount = totalUnitCount; + [promise.progress addChild:progress withPendingUnitCount:totalUnitCount]; + __auto_type resolver = ^(id __nullable value) { if ([value isKindOfClass:[FBLPromise class]]) { [(FBLPromise *)value observeOnQueue:queue @@ -268,7 +330,7 @@ static dispatch_queue_t gFBLPromiseDefaultDispatchQueue; }; [self observeOnQueue:queue fulfill:^(id __nullable value) { - value = chainedFulfill ? chainedFulfill(value) : value; + value = chainedFulfill ? chainedFulfill(value, progress) : value; resolver(value); } reject:^(NSError *error) { diff --git a/Sources/FBLPromises/FBLPromiseProgress.m b/Sources/FBLPromises/FBLPromiseProgress.m new file mode 100644 index 0000000..2300ae1 --- /dev/null +++ b/Sources/FBLPromises/FBLPromiseProgress.m @@ -0,0 +1,78 @@ +/** + Copyright 2018 Google Inc. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "FBLPromiseProgressPrivate.h" + +#import "FBLPromise.h" + +@implementation FBLPromiseProgress { + FBLPromise __weak *_promise; + NSHashTable *_syncProgresses; +} + +- (instancetype)initWithParent:(NSProgress *)parentProgressOrNil + userInfo:(NSDictionary *)userInfoOrNil { + self = [super initWithParent:parentProgressOrNil userInfo:userInfoOrNil]; + if (self) { + NSPointerFunctionsOptions options = + NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPersonality; + _syncProgresses = [[NSHashTable alloc] initWithOptions:options capacity:1]; + } + return self; +} + +- (void)addSyncProgress:(FBLPromiseProgress *)progress { + @synchronized(self) { + [_syncProgresses addObject:progress]; + } +} + +#pragma mark NSProgress + +- (void)setCompletedUnitCount:(int64_t)completedUnitCount { + @synchronized(self) { + super.completedUnitCount = completedUnitCount; + for (FBLPromiseProgress *progress in _syncProgresses) { + progress.completedUnitCount = completedUnitCount; + } + } +} + +- (void)setTotalUnitCount:(int64_t)totalUnitCount { + @synchronized(self) { + super.totalUnitCount = totalUnitCount; + for (FBLPromiseProgress *progress in _syncProgresses) { + progress.totalUnitCount = totalUnitCount; + } + } +} + +- (void)cancel { + @synchronized(self) { + if (!self.isCancelled) { + [super cancel]; + for (FBLPromiseProgress *progress in _syncProgresses) { + progress.cancellationHandler = nil; + [progress cancel]; + } + } + [_promise reject:[[NSError alloc] initWithDomain:FBLPromiseErrorDomain + code:FBLPromiseErrorCodeCancelled + userInfo:nil]]; + } +} + +@end diff --git a/Sources/FBLPromises/include/FBLPromise+Async.h b/Sources/FBLPromises/include/FBLPromise+Async.h index 0588a9e..59f831f 100644 --- a/Sources/FBLPromises/include/FBLPromise+Async.h +++ b/Sources/FBLPromises/include/FBLPromise+Async.h @@ -24,6 +24,9 @@ typedef void (^FBLPromiseFulfillBlock)(Value __nullable value) NS_SWIFT_UNAVAILA typedef void (^FBLPromiseRejectBlock)(NSError *error) NS_SWIFT_UNAVAILABLE(""); typedef void (^FBLPromiseAsyncWorkBlock)(FBLPromiseFulfillBlock fulfill, FBLPromiseRejectBlock reject) NS_SWIFT_UNAVAILABLE(""); +typedef void (^FBLPromiseAsyncProgressWorkBlock)(FBLPromiseFulfillBlock fulfill, + FBLPromiseRejectBlock reject, NSProgress *progress) + NS_SWIFT_UNAVAILABLE(""); /** Creates a pending promise and executes `work` block asynchronously. @@ -41,7 +44,14 @@ typedef void (^FBLPromiseAsyncWorkBlock)(FBLPromiseFulfillBlock fulfill, @return A new pending promise. */ + (instancetype)onQueue:(dispatch_queue_t)queue - async:(FBLPromiseAsyncWorkBlock)work NS_REFINED_FOR_SWIFT; + async:(FBLPromiseAsyncWorkBlock)work NS_SWIFT_UNAVAILABLE(""); + ++ (instancetype)progressUnits:(int64_t)totalUnitCount + async:(FBLPromiseAsyncProgressWorkBlock)work NS_SWIFT_UNAVAILABLE(""); + ++ (instancetype)onQueue:(dispatch_queue_t)queue + progressUnits:(int64_t)totalUnitCount + async:(FBLPromiseAsyncProgressWorkBlock)work NS_REFINED_FOR_SWIFT; @end @@ -54,6 +64,10 @@ typedef void (^FBLPromiseAsyncWorkBlock)(FBLPromiseFulfillBlock fulfill, + (FBLPromise* (^)(FBLPromiseAsyncWorkBlock))async FBL_PROMISES_DOT_SYNTAX NS_SWIFT_UNAVAILABLE(""); + (FBLPromise* (^)(dispatch_queue_t, FBLPromiseAsyncWorkBlock))asyncOn FBL_PROMISES_DOT_SYNTAX NS_SWIFT_UNAVAILABLE(""); ++ (FBLPromise* (^)(int64_t, FBLPromiseAsyncProgressWorkBlock))asyncProgress FBL_PROMISES_DOT_SYNTAX + NS_SWIFT_UNAVAILABLE(""); ++ (FBLPromise* (^)(dispatch_queue_t, int64_t, FBLPromiseAsyncProgressWorkBlock))asyncProgressOn + FBL_PROMISES_DOT_SYNTAX NS_SWIFT_UNAVAILABLE(""); @end diff --git a/Sources/FBLPromises/include/FBLPromise+Then.h b/Sources/FBLPromises/include/FBLPromise+Then.h index 32027e6..c6cfec5 100644 --- a/Sources/FBLPromises/include/FBLPromise+Then.h +++ b/Sources/FBLPromises/include/FBLPromise+Then.h @@ -21,6 +21,9 @@ NS_ASSUME_NONNULL_BEGIN @interface FBLPromise(ThenAdditions) typedef id __nullable (^FBLPromiseThenWorkBlock)(Value __nullable value) NS_SWIFT_UNAVAILABLE(""); +typedef id __nullable (^FBLPromiseThenProgressWorkBlock)(Value __nullable value, + NSProgress *progress) + NS_SWIFT_UNAVAILABLE(""); /** Creates a pending promise which eventually gets resolved with resolution returned from `work` @@ -44,7 +47,14 @@ typedef id __nullable (^FBLPromiseThenWorkBlock)(Value __nullable value) NS_SWIF @return A new pending promise to be resolved with resolution returned from the `work` block. */ - (FBLPromise *)onQueue:(dispatch_queue_t)queue - then:(FBLPromiseThenWorkBlock)work NS_REFINED_FOR_SWIFT; + then:(FBLPromiseThenWorkBlock)work NS_SWIFT_UNAVAILABLE(""); + +- (FBLPromise *)progressUnits:(int64_t)totalUnitCount + then:(FBLPromiseThenProgressWorkBlock)work NS_SWIFT_UNAVAILABLE(""); + +- (FBLPromise *)onQueue:(dispatch_queue_t)queue + progressUnits:(int64_t)totalUnitCount + then:(FBLPromiseThenProgressWorkBlock)work NS_REFINED_FOR_SWIFT; @end @@ -57,6 +67,10 @@ typedef id __nullable (^FBLPromiseThenWorkBlock)(Value __nullable value) NS_SWIF - (FBLPromise* (^)(FBLPromiseThenWorkBlock))then FBL_PROMISES_DOT_SYNTAX NS_SWIFT_UNAVAILABLE(""); - (FBLPromise* (^)(dispatch_queue_t, FBLPromiseThenWorkBlock))thenOn FBL_PROMISES_DOT_SYNTAX NS_SWIFT_UNAVAILABLE(""); +- (FBLPromise* (^)(int64_t, FBLPromiseThenProgressWorkBlock))thenProgress FBL_PROMISES_DOT_SYNTAX + NS_SWIFT_UNAVAILABLE(""); +- (FBLPromise* (^)(dispatch_queue_t, int64_t, FBLPromiseThenProgressWorkBlock))thenProgressOn + FBL_PROMISES_DOT_SYNTAX NS_SWIFT_UNAVAILABLE(""); @end diff --git a/Sources/FBLPromises/include/FBLPromise.h b/Sources/FBLPromises/include/FBLPromise.h index 6818671..6db44b6 100644 --- a/Sources/FBLPromises/include/FBLPromise.h +++ b/Sources/FBLPromises/include/FBLPromise.h @@ -28,6 +28,8 @@ NS_ASSUME_NONNULL_BEGIN */ @property(class) dispatch_queue_t defaultDispatchQueue NS_REFINED_FOR_SWIFT; +@property(nonatomic, readonly) NSProgress *progress NS_REFINED_FOR_SWIFT; + /** Creates a pending promise. */ @@ -55,6 +57,8 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reject:(NSError *)error NS_REFINED_FOR_SWIFT; +- (void)cancel NS_REFINED_FOR_SWIFT; + + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; diff --git a/Sources/FBLPromises/include/FBLPromiseError.h b/Sources/FBLPromises/include/FBLPromiseError.h index d0d47e9..c542ee3 100644 --- a/Sources/FBLPromises/include/FBLPromiseError.h +++ b/Sources/FBLPromises/include/FBLPromiseError.h @@ -24,12 +24,19 @@ extern NSErrorDomain const FBLPromiseErrorDomain NS_REFINED_FOR_SWIFT; Possible error codes in `FBLPromiseErrorDomain`. */ typedef NS_ENUM(NSInteger, FBLPromiseErrorCode) { + /** Promise was cancelled. */ + FBLPromiseErrorCodeCancelled = 1, /** Promise failed to resolve in time. */ - FBLPromiseErrorCodeTimedOut = 1, + FBLPromiseErrorCodeTimedOut = 2, /** Validation predicate returned false. */ - FBLPromiseErrorCodeValidationFailure = 2, + FBLPromiseErrorCodeValidationFailure = 3, } NS_REFINED_FOR_SWIFT; +NS_INLINE BOOL FBLPromiseErrorIsCancelled(NSError *error) NS_SWIFT_UNAVAILABLE("") { + return error.domain == FBLPromiseErrorDomain && + error.code == FBLPromiseErrorCodeCancelled; +} + NS_INLINE BOOL FBLPromiseErrorIsTimedOut(NSError *error) NS_SWIFT_UNAVAILABLE("") { return error.domain == FBLPromiseErrorDomain && error.code == FBLPromiseErrorCodeTimedOut; diff --git a/Sources/FBLPromises/include/FBLPromisePrivate.h b/Sources/FBLPromises/include/FBLPromisePrivate.h index 7a132f2..92f6a83 100644 --- a/Sources/FBLPromises/include/FBLPromisePrivate.h +++ b/Sources/FBLPromises/include/FBLPromisePrivate.h @@ -27,6 +27,9 @@ typedef void (^FBLPromiseOnFulfillBlock)(Value __nullable value) NS_SWIFT_UNAVAI typedef void (^FBLPromiseOnRejectBlock)(NSError *error) NS_SWIFT_UNAVAILABLE(""); typedef id __nullable (^__nullable FBLPromiseChainedFulfillBlock)(Value __nullable value) NS_SWIFT_UNAVAILABLE(""); +typedef id __nullable (^__nullable FBLPromiseChainedFulfillProgressBlock)(Value __nullable value, + NSProgress *progress) + NS_SWIFT_UNAVAILABLE(""); typedef id __nullable (^__nullable FBLPromiseChainedRejectBlock)(NSError *error) NS_SWIFT_UNAVAILABLE(""); @@ -61,6 +64,11 @@ typedef id __nullable (^__nullable FBLPromiseChainedRejectBlock)(NSError *error) chainedFulfill:(FBLPromiseChainedFulfillBlock)chainedFulfill chainedReject:(FBLPromiseChainedRejectBlock)chainedReject NS_SWIFT_UNAVAILABLE(""); +- (FBLPromise *)chainOnQueue:(dispatch_queue_t)queue + progressUnits:(int64_t)totalUnitCount + chainedFulfill:(FBLPromiseChainedFulfillProgressBlock)chainedFulfill + chainedReject:(FBLPromiseChainedRejectBlock)chainedReject NS_SWIFT_UNAVAILABLE(""); + @end NS_ASSUME_NONNULL_END diff --git a/Sources/FBLPromises/include/FBLPromiseProgress.h b/Sources/FBLPromises/include/FBLPromiseProgress.h new file mode 100644 index 0000000..bae99d5 --- /dev/null +++ b/Sources/FBLPromises/include/FBLPromiseProgress.h @@ -0,0 +1,28 @@ +/** + Copyright 2018 Google Inc. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBLPromiseProgress : NSProgress + ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/FBLPromises/include/FBLPromiseProgressPrivate.h b/Sources/FBLPromises/include/FBLPromiseProgressPrivate.h new file mode 100644 index 0000000..519cdc6 --- /dev/null +++ b/Sources/FBLPromises/include/FBLPromiseProgressPrivate.h @@ -0,0 +1,31 @@ +/** + Copyright 2018 Google Inc. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "FBLPromiseProgress.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FBLPromise; + +@interface FBLPromiseProgress() + +@property(weak) FBLPromise *promise; + +- (void)addSyncProgress:(FBLPromiseProgress *)progress; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/FBLPromises/include/framework.modulemap b/Sources/FBLPromises/include/framework.modulemap index 4976a2b..edd2e2e 100644 --- a/Sources/FBLPromises/include/framework.modulemap +++ b/Sources/FBLPromises/include/framework.modulemap @@ -3,6 +3,7 @@ framework module FBLPromises { header "FBLPromise.h" header "FBLPromiseError.h" + header "FBLPromiseProgress.h" header "FBLPromise+All.h" header "FBLPromise+Always.h" header "FBLPromise+Any.h" diff --git a/Sources/FBLPromises/include/module.modulemap b/Sources/FBLPromises/include/module.modulemap index af661c2..c25c9d8 100644 --- a/Sources/FBLPromises/include/module.modulemap +++ b/Sources/FBLPromises/include/module.modulemap @@ -3,6 +3,7 @@ module FBLPromises { header "FBLPromise.h" header "FBLPromiseError.h" + header "FBLPromiseProgress.h" header "FBLPromise+All.h" header "FBLPromise+Always.h" header "FBLPromise+Any.h" diff --git a/Sources/Promises/Promise+All.swift b/Sources/Promises/Promise+All.swift index 07cd292..4963ee2 100644 --- a/Sources/Promises/Promise+All.swift +++ b/Sources/Promises/Promise+All.swift @@ -74,7 +74,7 @@ public func all( Promise<(A, B)>.ObjCPromise.__onQueue( queue, all: promises - ).__onQueue(queue, then: { objCValues in + ).__onQueue(queue, progressUnits: 1, then: { objCValues, progress in guard let values = objCValues as [AnyObject]?, let valueA = Promise.asValue(values[0]), let valueB = Promise.asValue(values[1]) @@ -116,7 +116,7 @@ public func all( Promise<(A, B, C)>.ObjCPromise.__onQueue( queue, all: promises - ).__onQueue(queue, then: { objCValues in + ).__onQueue(queue, progressUnits: 1, then: { objCValues, progress in guard let values = objCValues as [AnyObject]?, let valueA = Promise.asValue(values[0]), let valueB = Promise.asValue(values[1]), diff --git a/Sources/Promises/Promise+Any.swift b/Sources/Promises/Promise+Any.swift index b367fe4..d10fea8 100644 --- a/Sources/Promises/Promise+Any.swift +++ b/Sources/Promises/Promise+Any.swift @@ -52,7 +52,7 @@ public func any( Promise<[Maybe]>.ObjCPromise.__onQueue( queue, any: promises - ).__onQueue(queue, then: { values in + ).__onQueue(queue, progressUnits: 1, then: { values, progress in guard let values = values as [AnyObject]? else { preconditionFailure() } return Promise<[Maybe]>.asAnyObject(values.map { asMaybe($0) as Maybe }) }) @@ -89,7 +89,7 @@ public func any( Promise<(Maybe, Maybe)>.ObjCPromise.__onQueue( queue, any: promises - ).__onQueue(queue, then: { objCValues in + ).__onQueue(queue, progressUnits: 1, then: { objCValues, progress in guard let values = objCValues as [AnyObject]? else { preconditionFailure() } let valueA = asMaybe(values[0]) as Maybe let valueB = asMaybe(values[1]) as Maybe @@ -131,7 +131,7 @@ public func any( Promise<(Maybe, Maybe, Maybe)>.ObjCPromise.__onQueue( queue, any: promises - ).__onQueue(queue, then: { objCValues in + ).__onQueue(queue, progressUnits: 1, then: { objCValues, progress in guard let values = objCValues as [AnyObject]? else { preconditionFailure() } let valueA = asMaybe(values[0]) as Maybe let valueB = asMaybe(values[1]) as Maybe diff --git a/Sources/Promises/Promise+Async.swift b/Sources/Promises/Promise+Async.swift index aa7227e..f78128e 100644 --- a/Sources/Promises/Promise+Async.swift +++ b/Sources/Promises/Promise+Async.swift @@ -15,14 +15,26 @@ import Foundation public extension Promise { + public typealias AsyncProgress = ( + @escaping (Value) -> Void, + @escaping (Error) -> Void, + Progress + ) throws -> Void public typealias Async = (@escaping (Value) -> Void, @escaping (Error) -> Void) throws -> Void /// Creates a pending promise and executes `work` block asynchronously on the given `queue`. /// - parameters: /// - queue: A queue to invoke the `work` block on. /// - work: A block to perform any operations needed to resolve the promise. - public convenience init(on queue: DispatchQueue = .promises, _ work: @escaping Async) { - let objCPromise = ObjCPromise.__onQueue(queue) { fulfill, reject in + public convenience init( + on queue: DispatchQueue = .promises, + progressUnits: Int64 = 1, + _ work: @escaping AsyncProgress + ) { + let objCPromise = ObjCPromise.__onQueue( + queue, + progressUnits: progressUnits + ) { fulfill, reject, progress in do { try work({ value in if let error = value as? NSError { @@ -30,7 +42,7 @@ public extension Promise { } else { fulfill(Promise.asAnyObject(value)) } - }, reject) + }, reject, progress) } catch let error { reject(error as NSError) } @@ -39,4 +51,10 @@ public extension Promise { // Keep Swift wrapper alive for chained promise until `ObjCPromise` counterpart is resolved. objCPromise.__pendingObjects?.add(self) } + + public convenience init(on queue: DispatchQueue = .promises, _ work: @escaping Async) { + self.init(on: queue) { fulfill, reject, _ in + try work(fulfill, reject) + } + } } diff --git a/Sources/Promises/Promise+Then.swift b/Sources/Promises/Promise+Then.swift index a3f4746..06e4b90 100644 --- a/Sources/Promises/Promise+Then.swift +++ b/Sources/Promises/Promise+Then.swift @@ -16,6 +16,7 @@ import Foundation public extension Promise { public typealias Then = (Value) throws -> Result + public typealias ThenProgress = (Value, Progress) throws -> Result /// Creates a pending promise which eventually gets resolved with the same resolution as the /// promise returned from `work` block. The `work` block is executed asynchronously on the given @@ -29,23 +30,34 @@ public extension Promise { @discardableResult public func then( on queue: DispatchQueue = .promises, - _ work: @escaping Then> + progressUnits: Int64 = 1, + _ work: @escaping ThenProgress> ) -> Promise { - let promise = Promise(objCPromise.__onQueue(queue, then: { objCValue in - guard let value = Promise.asValue(objCValue) else { - preconditionFailure("Cannot cast \(type(of: objCValue)) to \(Value.self)") - } - do { - return try work(value).objCPromise - } catch let error { - return error as NSError - } - })) + let promise = Promise( + objCPromise.__onQueue(queue, progressUnits: progressUnits, then: { objCValue, progress in + guard let value = Promise.asValue(objCValue) else { + preconditionFailure("Cannot cast \(type(of: objCValue)) to \(Value.self)") + } + do { + return try work(value, progress).objCPromise + } catch let error { + return error as NSError + } + }) + ) // Keep Swift wrapper alive for chained promise until `ObjCPromise` counterpart is resolved. objCPromise.__pendingObjects?.add(promise) return promise } + @discardableResult + public func then( + on queue: DispatchQueue = .promises, + _ work: @escaping Then> + ) -> Promise { + return then(on: queue) { value, _ in try work(value) } + } + /// Creates a pending promise which eventually gets resolved with the value returned from `work` /// block. The `work` block is executed asynchronously on the given `queue` only when `self` is /// fulfilled. If `self` is rejected, the returned promise is also rejected with the same error. @@ -56,24 +68,35 @@ public extension Promise { @discardableResult public func then( on queue: DispatchQueue = .promises, - _ work: @escaping Then + progressUnits: Int64 = 1, + _ work: @escaping ThenProgress ) -> Promise { - let promise = Promise(objCPromise.__onQueue(queue, then: { objCValue in - guard let value = Promise.asValue(objCValue) else { - preconditionFailure("Cannot cast \(type(of: objCValue)) to \(Value.self)") - } - do { - let value = try work(value) - return value as? NSError ?? Promise.asAnyObject(value) - } catch let error { - return error as NSError - } - })) + let promise = Promise( + objCPromise.__onQueue(queue, progressUnits: progressUnits, then: { objCValue, progress in + guard let value = Promise.asValue(objCValue) else { + preconditionFailure("Cannot cast \(type(of: objCValue)) to \(Value.self)") + } + do { + let value = try work(value, progress) + return value as? NSError ?? Promise.asAnyObject(value) + } catch let error { + return error as NSError + } + }) + ) // Keep Swift wrapper alive for chained promise until `ObjCPromise` counterpart is resolved. objCPromise.__pendingObjects?.add(promise) return promise } + @discardableResult + public func then( + on queue: DispatchQueue = .promises, + _ work: @escaping Then + ) -> Promise { + return then(on: queue) { value, _ in try work(value) } + } + /// Creates a pending promise which eventually gets resolved with the same resolution as `self`. /// `work` block is executed asynchronously on the given `queue` only when `self` is fulfilled. /// If `self` is rejected, the returned promise is also rejected with the same error. @@ -84,21 +107,32 @@ public extension Promise { @discardableResult public func then( on queue: DispatchQueue = .promises, - _ work: @escaping Then + progressUnits: Int64 = 1, + _ work: @escaping ThenProgress ) -> Promise { - let promise = Promise(objCPromise.__onQueue(queue, then: { objCValue in - guard let value = Promise.asValue(objCValue) else { - preconditionFailure("Cannot cast \(type(of: objCValue)) to \(Value.self)") - } - do { - try work(value) - return Promise.asAnyObject(value) - } catch let error { - return error as NSError - } - })) + let promise = Promise( + objCPromise.__onQueue(queue, progressUnits: progressUnits, then: { objCValue, progress in + guard let value = Promise.asValue(objCValue) else { + preconditionFailure("Cannot cast \(type(of: objCValue)) to \(Value.self)") + } + do { + try work(value, progress) + return Promise.asAnyObject(value) + } catch let error { + return error as NSError + } + }) + ) // Keep Swift wrapper alive for chained promise until `ObjCPromise` counterpart is resolved. objCPromise.__pendingObjects?.add(promise) return promise } + + @discardableResult + public func then( + on queue: DispatchQueue = .promises, + _ work: @escaping Then + ) -> Promise { + return then(on: queue) { value, _ in try work(value) } + } } diff --git a/Sources/Promises/Promise.swift b/Sources/Promises/Promise.swift index a4ab15d..1467eb4 100644 --- a/Sources/Promises/Promise.swift +++ b/Sources/Promises/Promise.swift @@ -18,6 +18,8 @@ import FBLPromises public final class Promise { public typealias ObjCPromise = FBLPromise + public var progress: Progress { return objCPromise.__progress } + /// Creates a new promise with an existing ObjC promise. public init(_ objCPromise: ObjCPromise) { guard let objCPromise = objCPromise as? ObjCPromise else { @@ -63,6 +65,10 @@ public final class Promise { objCPromise.__fulfill(error as NSError) } + public func cancel() { + objCPromise.__cancel() + } + /// Converts `self` into ObjC promise. public func asObjCPromise() -> ObjCPromise { guard let objCPromise = objCPromise as? ObjCPromise else { diff --git a/Tests/FBLPromisesTests/FBLPromise+ProgressTests.m b/Tests/FBLPromisesTests/FBLPromise+ProgressTests.m new file mode 100644 index 0000000..83efba1 --- /dev/null +++ b/Tests/FBLPromisesTests/FBLPromise+ProgressTests.m @@ -0,0 +1,63 @@ +/** + Copyright 2018 Google Inc. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +#import "FBLPromise+Async.h" +#import "FBLPromise+Testing.h" +#import "FBLPromise+Then.h" +#import "FBLPromise+Wrap.h" +#import "FBLPromisesTestHelpers.h" + +@interface FBLPromiseProgressTests : XCTestCase +@end + +@implementation FBLPromiseProgressTests + +- (void)testPromiseProgressCancel { + // Arrange. + FBLPromise *promise = [[FBLPromise + progressUnits:10 + async:^(FBLPromiseFulfillBlock fulfill, FBLPromiseRejectBlock reject, + NSProgress *progress) { + NSURLSessionDataTask *task = [NSURLSession.sharedSession + dataTaskWithURL:[NSURL URLWithString:@"https://google.com"] + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + reject(error); + } else { + fulfill(@[ data, response ]); + } + }]; + [progress addChild:task.progress withPendingUnitCount:progress.totalUnitCount]; + [task resume]; + }] progressUnits:10 + then:^id(id value, NSProgress *progress) { + progress.completedUnitCount += 10; + return nil; + }]; + + // Act. + FBLDelay(5, ^{ + [promise cancel]; + }); + + // Assert. + XCTAssert(FBLWaitForPromisesWithTimeout(10)); + NSLog(@"%@", promise); +} + +@end diff --git a/Tests/PromisesTests/Promise+ProgressTests.swift b/Tests/PromisesTests/Promise+ProgressTests.swift new file mode 100644 index 0000000..5bb2d65 --- /dev/null +++ b/Tests/PromisesTests/Promise+ProgressTests.swift @@ -0,0 +1,51 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import PromisesTestHelpers +import XCTest +@testable import Promises + +class PromiseProgressTests: XCTestCase { + func testPromiseProgressCancel() { + // Arrange. + let promise = Promise<(Data?, URLResponse?)>(progressUnits: 10) { fulfill, reject, progress in + let task = URLSession.shared.dataTask( + with:URL(string: "https://google.com")! + ) { data, response, error in + if let error = error { + reject(error) + } else { + fulfill((data, response)) + } + } + if #available(iOS 11.0, *) { + progress.addChild(task.progress, withPendingUnitCount: progress.totalUnitCount) + } else { + // Fallback on earlier versions + } + task.resume() + }.then { value, progress in + progress.completedUnitCount += 5 + } + + // Act. + Test.delay(5) { + promise.cancel() + } + + // Assert. + XCTAssert(waitForPromises(timeout: 10)) + print(promise) + } +}