diff --git a/SwiftTask.xcodeproj/project.pbxproj b/SwiftTask.xcodeproj/project.pbxproj index 4a596da..55d2778 100644 --- a/SwiftTask.xcodeproj/project.pbxproj +++ b/SwiftTask.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 1F20250219ADA8FD00DE0495 /* BasicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F20250119ADA8FD00DE0495 /* BasicTests.swift */; }; + 1F218D5B1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */; }; + 1F218D5C1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */; }; 1F46DEDA199EDF1000F97868 /* SwiftTask.h in Headers */ = {isa = PBXBuildFile; fileRef = 1F46DED9199EDF1000F97868 /* SwiftTask.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1F46DEFB199EDF8100F97868 /* SwiftTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F46DEFA199EDF8100F97868 /* SwiftTask.swift */; }; 1F46DEFD199EE2C200F97868 /* _TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F46DEFC199EE2C200F97868 /* _TestCase.swift */; }; @@ -18,6 +20,8 @@ 1FCF71141AD8CD2F007079C2 /* Async.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCF71131AD8CD2F007079C2 /* Async.framework */; }; 1FCF71161AD8CD38007079C2 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCF71151AD8CD38007079C2 /* Alamofire.framework */; }; 1FCF71181AD8CD3C007079C2 /* Async.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCF71171AD8CD3C007079C2 /* Async.framework */; }; + 1FD7197B1AFE387C00BC38C4 /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD7197A1AFE387C00BC38C4 /* Cancellable.swift */; }; + 1FD7197C1AFE387C00BC38C4 /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD7197A1AFE387C00BC38C4 /* Cancellable.swift */; }; 1FF52EB41A4C395A00B4BA28 /* _InterruptableTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF52EB31A4C395A00B4BA28 /* _InterruptableTask.swift */; }; 1FF52EB51A4C395A00B4BA28 /* _InterruptableTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF52EB31A4C395A00B4BA28 /* _InterruptableTask.swift */; }; 4822F0DC19D00B2300F5F572 /* _TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F46DEFC199EE2C200F97868 /* _TestCase.swift */; }; @@ -37,6 +41,7 @@ /* Begin PBXFileReference section */ 1F20250119ADA8FD00DE0495 /* BasicTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicTests.swift; sourceTree = ""; }; + 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveHandlerTests.swift; sourceTree = ""; }; 1F46DED4199EDF1000F97868 /* SwiftTask.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftTask.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1F46DED8199EDF1000F97868 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1F46DED9199EDF1000F97868 /* SwiftTask.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftTask.h; sourceTree = ""; }; @@ -50,6 +55,7 @@ 1FCF71131AD8CD2F007079C2 /* Async.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Async.framework; path = "../Carthage/Checkouts/Async/build/Debug-iphoneos/Async.framework"; sourceTree = ""; }; 1FCF71151AD8CD38007079C2 /* Alamofire.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Alamofire.framework; path = ../Carthage/Checkouts/Alamofire/build/Debug/Alamofire.framework; sourceTree = ""; }; 1FCF71171AD8CD3C007079C2 /* Async.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Async.framework; path = ../Carthage/Checkouts/Async/build/Debug/Async.framework; sourceTree = ""; }; + 1FD7197A1AFE387C00BC38C4 /* Cancellable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cancellable.swift; sourceTree = ""; }; 1FF52EB31A4C395A00B4BA28 /* _InterruptableTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _InterruptableTask.swift; sourceTree = ""; }; 4822F0D019D00ABF00F5F572 /* SwiftTask-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SwiftTask-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 48511C5A19C17563002FE03C /* RetainCycleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RetainCycleTests.swift; sourceTree = ""; }; @@ -121,6 +127,7 @@ children = ( 1F46DED9199EDF1000F97868 /* SwiftTask.h */, 1F46DEFA199EDF8100F97868 /* SwiftTask.swift */, + 1FD7197A1AFE387C00BC38C4 /* Cancellable.swift */, 48B58D7A1A6F255E0068E18C /* _StateMachine.swift */, 1F46DED7199EDF1000F97868 /* Supporting Files */, ); @@ -144,6 +151,7 @@ 1F46DEE3199EDF1000F97868 /* SwiftTaskTests.swift */, 48511C5A19C17563002FE03C /* RetainCycleTests.swift */, 485C31F01A1D619A00040DA3 /* TypeInferenceTests.swift */, + 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */, 1F5FA35619A374E600975FB9 /* AlamofireTests.swift */, 1F46DEE1199EDF1000F97868 /* Supporting Files */, ); @@ -335,6 +343,7 @@ buildActionMask = 2147483647; files = ( 1F46DEFB199EDF8100F97868 /* SwiftTask.swift in Sources */, + 1FD7197B1AFE387C00BC38C4 /* Cancellable.swift in Sources */, 48B58D7B1A6F255E0068E18C /* _StateMachine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -345,6 +354,7 @@ files = ( 1F20250219ADA8FD00DE0495 /* BasicTests.swift in Sources */, 1FF52EB41A4C395A00B4BA28 /* _InterruptableTask.swift in Sources */, + 1F218D5B1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */, 1F6A8CA319A4E4F200369A5D /* SwiftTaskTests.swift in Sources */, 1F4C76A41AD8CF40004E47C1 /* AlamofireTests.swift in Sources */, 485C31F11A1D619A00040DA3 /* TypeInferenceTests.swift in Sources */, @@ -359,6 +369,7 @@ files = ( 4822F0DE19D00B2300F5F572 /* SwiftTaskTests.swift in Sources */, 4822F0DD19D00B2300F5F572 /* BasicTests.swift in Sources */, + 1F218D5C1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */, 1FF52EB51A4C395A00B4BA28 /* _InterruptableTask.swift in Sources */, 1F4C76A51AD8CF41004E47C1 /* AlamofireTests.swift in Sources */, 485C31F21A1D619A00040DA3 /* TypeInferenceTests.swift in Sources */, @@ -372,6 +383,7 @@ buildActionMask = 2147483647; files = ( 48CD5A3C19AEEBDF0042B9F1 /* SwiftTask.swift in Sources */, + 1FD7197C1AFE387C00BC38C4 /* Cancellable.swift in Sources */, 48B58D7C1A6F255E0068E18C /* _StateMachine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SwiftTask/Cancellable.swift b/SwiftTask/Cancellable.swift new file mode 100644 index 0000000..a707f16 --- /dev/null +++ b/SwiftTask/Cancellable.swift @@ -0,0 +1,56 @@ +// +// Cancellable.swift +// SwiftTask +// +// Created by Yasuhiro Inami on 2015/05/09. +// Copyright (c) 2015年 Yasuhiro Inami. All rights reserved. +// + +import Foundation + +public protocol Cancellable +{ + typealias Error + + // + // NOTE: + // Single `func cancel(error: Error) -> Bool` is preferred (as first implemented in 8a22ed5), + // but two overloaded methods are required for SwiftTask ver 3.x API compatibility. + // + func cancel() -> Bool + func cancel(#error: Error) -> Bool +} + +public class Canceller: Cancellable +{ + private var cancelHandler: (Void -> Void)? + + public required init(cancelHandler: Void -> Void) + { + self.cancelHandler = cancelHandler + } + + public func cancel() -> Bool + { + return self.cancel(error: ()) + } + + public func cancel(#error: Void) -> Bool + { + if let cancelHandler = self.cancelHandler { + self.cancelHandler = nil + cancelHandler() + return true + } + + return false + } +} + +public class AutoCanceller: Canceller +{ + deinit + { + self.cancel() + } +} \ No newline at end of file diff --git a/SwiftTask/SwiftTask.swift b/SwiftTask/SwiftTask.swift index d99f26d..503151a 100644 --- a/SwiftTask/SwiftTask.swift +++ b/SwiftTask/SwiftTask.swift @@ -57,7 +57,7 @@ public class TaskConfiguration } } -public class Task: Printable +public class Task: Cancellable, Printable { public typealias ProgressTuple = (oldProgress: Progress?, newProgress: Progress) public typealias ErrorInfo = (error: Error?, isCancelled: Bool) @@ -213,7 +213,8 @@ public class Task: Printable internal func setup(#weakified: Bool, paused: Bool, _initClosure: _InitClosure) { // #if DEBUG -// println("[init] \(self.name)") +// let addr = String(format: "%p", unsafeAddressOf(self)) +// NSLog("[init] \(self.name) \(addr)") // #endif self._initClosure = _initClosure @@ -287,7 +288,8 @@ public class Task: Printable deinit { // #if DEBUG -// println("[deinit] \(self.name)") +// let addr = String(format: "%p", unsafeAddressOf(self)) +// NSLog("[deinit] \(self.name) \(addr)") // #endif // cancel in case machine is still running @@ -355,31 +357,62 @@ public class Task: Printable /// public func progress(progressClosure: ProgressTuple -> Void) -> Task { - self._machine.addProgressTupleHandler(progressClosure) + var dummyCanceller: Canceller? = nil + return self.progress(&dummyCanceller, progressClosure) + } + + public func progress(inout canceller: C?, _ progressClosure: ProgressTuple -> Void) -> Task + { + var token: _HandlerToken? = nil + self._machine.addProgressTupleHandler(&token, progressClosure) + + canceller = C { [weak self] in + self?._machine.removeProgressTupleHandler(token) + } return self } /// - /// then (fulfilled & rejected) + closure returning value + /// then (fulfilled & rejected) + closure returning **value** + /// (a.k.a. `map` in functional programming term) /// /// - e.g. task.then { value, errorInfo -> NextValueType in ... } /// public func then(thenClosure: (Value?, ErrorInfo?) -> Value2) -> Task { - return self.then { (value: Value?, errorInfo: ErrorInfo?) -> Task in + var dummyCanceller: Canceller? = nil + return self.then(&dummyCanceller, thenClosure) + } + + public func then(inout canceller: C?, _ thenClosure: (Value?, ErrorInfo?) -> Value2) -> Task + { + return self.then(&canceller) { (value: Value?, errorInfo: ErrorInfo?) -> Task in return Task(value: thenClosure(value, errorInfo)) } } /// - /// then (fulfilled & rejected) + closure returning task + /// then (fulfilled & rejected) + closure returning **task** + /// (a.k.a. `flatMap` in functional programming term) /// /// - e.g. task.then { value, errorInfo -> NextTaskType in ... } /// public func then(thenClosure: (Value?, ErrorInfo?) -> Task) -> Task { - return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in + var dummyCanceller: Canceller? = nil + return self.then(&dummyCanceller, thenClosure) + } + + // + // NOTE: then-canceller is a shorthand of `task.cancel(nil)`, i.e. these two are the same: + // + // - `let canceller = Canceller(); task1.then(&canceller) {...}; canceller.cancel();` + // - `let task2 = task1.then {...}; task2.cancel();` + // + public func then(inout canceller: C?, _ thenClosure: (Value?, ErrorInfo?) -> Task) -> Task + { + return Task { [unowned self, weak canceller] newMachine, progress, fulfill, _reject, configure in // // NOTE: @@ -389,8 +422,8 @@ public class Task: Printable // This is especially important for ReactKit's `deinitSignal` behavior. // let selfMachine = self._machine - - self._then { + + self._then(&canceller) { let innerTask = thenClosure(selfMachine.value, selfMachine.errorInfo) _bindInnerTask(innerTask, newMachine, progress, fulfill, _reject, configure) } @@ -399,41 +432,58 @@ public class Task: Printable } /// invokes `completionHandler` "now" or "in the future" - private func _then(completionHandler: Void -> Void) + private func _then(inout canceller: C?, _ completionHandler: Void -> Void) { switch self.state { case .Fulfilled, .Rejected, .Cancelled: completionHandler() default: - self._machine.addCompletionHandler(completionHandler) + var token: _HandlerToken? = nil + self._machine.addCompletionHandler(&token, completionHandler) + + canceller = C { [weak self] in + self?._machine.removeCompletionHandler(token) + } } } /// - /// success (fulfilled) + closure returning value + /// success (fulfilled) + closure returning **value** /// /// - e.g. task.success { value -> NextValueType in ... } /// public func success(successClosure: Value -> Value2) -> Task { - return self.success { (value: Value) -> Task in + var dummyCanceller: Canceller? = nil + return self.success(&dummyCanceller, successClosure) + } + + public func success(inout canceller: C?, _ successClosure: Value -> Value2) -> Task + { + return self.success(&canceller) { (value: Value) -> Task in return Task(value: successClosure(value)) } } /// - /// success (fulfilled) + closure returning task + /// success (fulfilled) + closure returning **task** /// /// - e.g. task.success { value -> NextTaskType in ... } /// public func success(successClosure: Value -> Task) -> Task + { + var dummyCanceller: Canceller? = nil + return self.success(&dummyCanceller, successClosure) + } + + public func success(inout canceller: C?, _ successClosure: Value -> Task) -> Task { return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in let selfMachine = self._machine // NOTE: using `self._then()` + `selfMachine` instead of `self.then()` will reduce Task allocation - self._then { + self._then(&canceller) { if let value = selfMachine.value { let innerTask = successClosure(value) _bindInnerTask(innerTask, newMachine, progress, fulfill, _reject, configure) @@ -447,31 +497,43 @@ public class Task: Printable } /// - /// failure (rejected) + closure returning value + /// failure (rejected or cancelled) + closure returning **value** /// /// - e.g. task.failure { errorInfo -> NextValueType in ... } /// - e.g. task.failure { error, isCancelled -> NextValueType in ... } /// public func failure(failureClosure: ErrorInfo -> Value) -> Task { - return self.failure { (errorInfo: ErrorInfo) -> Task in + var dummyCanceller: Canceller? = nil + return self.failure(&dummyCanceller, failureClosure) + } + + public func failure(inout canceller: C?, _ failureClosure: ErrorInfo -> Value) -> Task + { + return self.failure(&canceller) { (errorInfo: ErrorInfo) -> Task in return Task(value: failureClosure(errorInfo)) } } /// - /// failure (rejected) + closure returning task + /// failure (rejected or cancelled) + closure returning **task** /// /// - e.g. task.failure { errorInfo -> NextTaskType in ... } /// - e.g. task.failure { error, isCancelled -> NextTaskType in ... } /// public func failure(failureClosure: ErrorInfo -> Task) -> Task + { + var dummyCanceller: Canceller? = nil + return self.failure(&dummyCanceller, failureClosure) + } + + public func failure(inout canceller: C?, _ failureClosure: ErrorInfo -> Task) -> Task { return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in let selfMachine = self._machine - self._then { + self._then(&canceller) { if let value = selfMachine.value { fulfill(value) } @@ -494,7 +556,18 @@ public class Task: Printable return self._machine.handleResume() } - public func cancel(error: Error? = nil) -> Bool + // + // NOTE: + // To conform to `Cancellable`, this method is needed in replace of: + // - `public func cancel(error: Error? = nil) -> Bool` + // - `public func cancel(_ error: Error? = nil) -> Bool` (segfault in Swift 1.2) + // + public func cancel() -> Bool + { + return self.cancel(error: nil) + } + + public func cancel(#error: Error?) -> Bool { return self._cancel(error: error) } @@ -503,6 +576,7 @@ public class Task: Printable { return self._machine.handleCancel(error: error) } + } // MARK: - Helper diff --git a/SwiftTask/_StateMachine.swift b/SwiftTask/_StateMachine.swift index 7f204da..ba1da0e 100644 --- a/SwiftTask/_StateMachine.swift +++ b/SwiftTask/_StateMachine.swift @@ -30,8 +30,8 @@ internal class _StateMachine /// and will be set to `nil` afterward internal var initResumeClosure: (Void -> Void)? - internal private(set) lazy var progressTupleHandlers: [ProgressTupleHandler] = [] - internal private(set) lazy var completionHandlers: [Void -> Void] = [] + internal private(set) lazy var progressTupleHandlers = _Handlers() + internal private(set) lazy var completionHandlers = _Handlers Void>() internal let configuration = TaskConfiguration() @@ -41,18 +41,40 @@ internal class _StateMachine self.state = paused ? .Paused : .Running } - internal func addProgressTupleHandler(progressTupleHandler: ProgressTupleHandler) + internal func addProgressTupleHandler(inout token: _HandlerToken?, _ progressTupleHandler: ProgressTupleHandler) -> Bool { if self.state == .Running || self.state == .Paused { - self.progressTupleHandlers.append(progressTupleHandler) + token = self.progressTupleHandlers.append(progressTupleHandler) + return token != nil } + return false } - internal func addCompletionHandler(completionHandler: Void -> Void) + internal func removeProgressTupleHandler(handlerToken: _HandlerToken?) -> Bool + { + if let handlerToken = handlerToken { + let removedHandler = self.progressTupleHandlers.remove(handlerToken) + return removedHandler != nil + } + return false + } + + internal func addCompletionHandler(inout token: _HandlerToken?, _ completionHandler: Void -> Void) -> Bool { if self.state == .Running || self.state == .Paused { - self.completionHandlers.append(completionHandler) + token = self.completionHandlers.append(completionHandler) + return token != nil + } + return false + } + + internal func removeCompletionHandler(handlerToken: _HandlerToken?) -> Bool + { + if let handlerToken = handlerToken { + let removedHandler = self.completionHandlers.remove(handlerToken) + return removedHandler != nil } + return false } internal func handleProgress(progress: Progress) @@ -185,4 +207,50 @@ internal class _StateMachine self.initResumeClosure = nil self.progress = nil } +} + +//-------------------------------------------------- +// MARK: - Utility +//-------------------------------------------------- + +internal struct _HandlerToken +{ + internal let key: Int +} + +internal struct _Handlers: SequenceType +{ + internal typealias KeyValue = (key: Int, value: T) + + private var currentKey: Int = 0 + private var elements = [KeyValue]() + + internal mutating func append(value: T) -> _HandlerToken + { + self.currentKey = self.currentKey &+ 1 + + self.elements += [(key: self.currentKey, value: value)] + + return _HandlerToken(key: self.currentKey) + } + + internal mutating func remove(token: _HandlerToken) -> T? + { + for var i = 0; i < self.elements.count; i++ { + if self.elements[i].key == token.key { + return self.elements.removeAtIndex(i).value + } + } + return nil + } + + internal mutating func removeAll(keepCapacity: Bool = false) + { + self.elements.removeAll(keepCapacity: keepCapacity) + } + + internal func generate() -> GeneratorOf + { + return GeneratorOf(self.elements.map { $0.value }.generate()) + } } \ No newline at end of file diff --git a/SwiftTaskTests/RemoveHandlerTests.swift b/SwiftTaskTests/RemoveHandlerTests.swift new file mode 100644 index 0000000..7345b6a --- /dev/null +++ b/SwiftTaskTests/RemoveHandlerTests.swift @@ -0,0 +1,106 @@ +// +// RemoveHandlerTests.swift +// SwiftTask +// +// Created by Yasuhiro Inami on 2015/05/28. +// Copyright (c) 2015年 Yasuhiro Inami. All rights reserved. +// + +import SwiftTask +import Async +import XCTest + +class RemoveHandlerTests: _TestCase +{ + func testRemoveProgress() + { + typealias Task = SwiftTask.Task + + var expect = self.expectationWithDescription(__FUNCTION__) + + var latestProgressValue: Float? + var canceller: AutoCanceller? = nil + + // define task + Task { progress, fulfill, reject, configure in + progress(0.0) + Async.main(after: 0.1) { + progress(1.0) + fulfill("OK") + } + }.progress { oldProgress, newProgress in + + println("progress1 = \(newProgress)") + latestProgressValue = newProgress + + }.progress(&canceller) { oldProgress, newProgress in + + println("progress2 = \(newProgress)") + XCTFail("Should never reach here because this progress-handler will be removed soon.") + + }.then { value, errorInfo -> Void in + + XCTAssertTrue(value == "OK") + XCTAssertTrue(errorInfo == nil) + expect.fulfill() + + } + + XCTAssertTrue(canceller != nil, "Async `task` will return non-nil `progressToken`.") + + // remove progress-handler + canceller = nil + + self.wait() + + XCTAssertTrue(latestProgressValue == 1.0) + } + + func testRemoveThen() + { + typealias Task = SwiftTask.Task + + var expect = self.expectationWithDescription(__FUNCTION__) + var canceller: AutoCanceller? = nil + + // define task + Task { progress, fulfill, reject, configure in + + Async.main(after: 0.1) { + fulfill("OK") + } + return + + }.success { value -> String in + + XCTAssertEqual(value, "OK") + return "Now OK" + + }.then(&canceller) { value, errorInfo -> String in + + println("Should never reach here") + + XCTFail("Should never reach here because this then-handler will be removed soon.") + + return "Never reaches here" + + }.then { value, errorInfo -> Void in + + println("value = \(value)") + println("errorInfo = \(errorInfo)") + + XCTAssertTrue(value == nil) + XCTAssertTrue(errorInfo != nil) + XCTAssertTrue(errorInfo!.error == nil) + XCTAssertTrue(errorInfo!.isCancelled, "Deallocation of `canceller` will force `task2` (where `task2 = task.then(&canceller)`) to deinit immediately and tries cancellation if it is still running.") + + expect.fulfill() + + } + + // remove then-handler + canceller = nil + + self.wait() + } +} \ No newline at end of file