diff --git a/CommonInternal/CancelableProcedure.swift b/CommonInternal/CancelableProcedure.swift new file mode 100644 index 0000000..df6bac1 --- /dev/null +++ b/CommonInternal/CancelableProcedure.swift @@ -0,0 +1,43 @@ +/// Encapsulate the function and make it cancelable. +/// A function will not be executed after canceling, except already in progress execution. +final class CancelableProcedure { + /// A Bool value indicating whether canceled. + var isCanceled: Bool { + return _isCanceled.value + } + + private let _isCanceled: AtomicBool = false + private var _execute: ((Value) -> Void)? + + /// Initialize with an arbitrary function. + /// + /// - Parameters: + /// - execute: A function to be executed by calling `execute(with:)` until canceled. + init(_ execute: @escaping (Value) -> Void) { + _execute = execute + } + + /// Synchronously execute the specified function. + /// + /// - Parameters: + /// - value: A value to be pass to specified function. + func execute(with value: @autoclosure () -> Value) { + guard !isCanceled, let execute = _execute else { return } + execute(value()) + } + + /// Cancel the specified function. + /// Cancellation does not affect already in progress execution. + func cancel() { + guard _isCanceled.compareAndSwapBarrier(old: false, new: true) else { return } + _execute = nil + } +} + +extension CancelableProcedure where Value == Void { + /// Synchronously execute the specified function. + @inline(__always) + func execute() { + self.execute(with: ()) + } +} diff --git a/README.md b/README.md index 4bff6df..98470ae 100644 --- a/README.md +++ b/README.md @@ -281,26 +281,6 @@ store.computed.valueSignal } ``` -**Executor.WorkItem** -Executor can also be executed with WorkItem. -The WorkItem can call `cancel` to prevent it from execute in future. -Cancellation does not affect any execution of the function that is already in progress. - -```swift -let executor = Executor.immediate -let workItem = Executor.WorkItem { value in - print("Value is \(value)") -} - -executor.execute(workItem: workItem, with: 100) - -workItem.cancel() - -executor.execute(workItem: workItem, with: 200) - -// prints "Value is 100" -``` - ### Signal Operators VueFluxReactive restricts functional approach AMAP. However, includes minimum operators for convenience. diff --git a/Tests/CancelableProcedureTests.swift b/Tests/CancelableProcedureTests.swift new file mode 100644 index 0000000..610b227 --- /dev/null +++ b/Tests/CancelableProcedureTests.swift @@ -0,0 +1,131 @@ +import XCTest +@testable import VueFlux +@testable import VueFluxReactive + +private protocol CancelableProcedureProtocol { + associatedtype Value + + var isCanceled: Bool { get } + + init(_ execute: @escaping (Value) -> Void) + func execute(with value: @autoclosure () -> Value) + func cancel() +} + +extension VueFlux.CancelableProcedure: CancelableProcedureProtocol {} +extension VueFluxReactive.CancelableProcedure: CancelableProcedureProtocol {} + +final class CancelableProcedureTests: XCTestCase { + func testVoidProcedure() { + func runTest(for type: CancelableProcedure.Type) where CancelableProcedure.Value == Void { + + var value = 0 + + let procedure = CancelableProcedure { + value += 1 + } + + XCTAssertEqual(value, 0) + + procedure.execute(with: ()) + + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute(with: ()) + + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute(with: ()) + + XCTAssertEqual(value, 1) + } + + runTest(for: VueFlux.CancelableProcedure.self) + runTest(for: VueFluxReactive.CancelableProcedure.self) + } + + func testValueWorkItem() { + func runTest(for type: CancelableProcedure.Type) where CancelableProcedure.Value == Int { + var value = 0 + + let procedure = CancelableProcedure { int in + value = int + } + + XCTAssertEqual(value, 0) + + procedure.execute(with: 1) + + XCTAssertFalse(procedure.isCanceled) + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute(with: 2) + + XCTAssertTrue(procedure.isCanceled) + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute(with: 3) + + XCTAssertTrue(procedure.isCanceled) + XCTAssertEqual(value, 1) + } + + runTest(for: VueFlux.CancelableProcedure.self) + runTest(for: VueFluxReactive.CancelableProcedure.self) + } + + func testCancelProcedureAsync() { + func runTest(for type: CancelableProcedure.Type) where CancelableProcedure.Value == Int { + let queue = DispatchQueue(label: "testCancelProcedureAsync") + + var value = 0 + + let expectation = self.expectation(description: "testCancelProcedureAsync") + + let procedure = CancelableProcedure { int in + value = int + } + + XCTAssertFalse(procedure.isCanceled) + + queue.suspend() + + queue.async { + procedure.execute(with: 1) + } + + procedure.cancel() + queue.resume() + + queue.async(execute: expectation.fulfill) + + waitForExpectations(timeout: 1) { _ in + XCTAssertTrue(procedure.isCanceled) + XCTAssertEqual(value, 0) + } + } + + runTest(for: VueFlux.CancelableProcedure.self) + runTest(for: VueFluxReactive.CancelableProcedure.self) + } + + func testExecuteVoidProcedure() { + var value = 0 + let vueFluxProcedure = VueFlux.CancelableProcedure { + value = 1 + } + + let vueFluxReactiveProcedure = VueFluxReactive.CancelableProcedure { + value = 2 + } + + vueFluxProcedure.execute() + vueFluxReactiveProcedure.execute() + + XCTAssertEqual(value, 2) + } +} diff --git a/Tests/DisposableTests.swift b/Tests/DisposableTests.swift index 39cb781..435d4b0 100644 --- a/Tests/DisposableTests.swift +++ b/Tests/DisposableTests.swift @@ -21,4 +21,15 @@ final class DisposableTests: XCTestCase { XCTAssertEqual(value, 1) XCTAssertEqual(disposable.isDisposed, true) } + + func testNopDisposable() { + let disposable = AnyDisposable() + + XCTAssertEqual(disposable.isDisposed, false) + + disposable.dispose() + + XCTAssertEqual(disposable.isDisposed, true) + + } } diff --git a/Tests/ExecutorTests.swift b/Tests/ExecutorTests.swift index 4ef09db..fec33fa 100644 --- a/Tests/ExecutorTests.swift +++ b/Tests/ExecutorTests.swift @@ -84,102 +84,4 @@ final class ExecutorTests: XCTestCase { XCTAssertEqual(value, 1) } } - - func testVoidWorkItem() { - var value = 0 - - let workItem = Executor.WorkItem { - value = 1 - } - - XCTAssertEqual(value, 0) - - workItem.execute() - - XCTAssertEqual(value, 1) - } - - func testValueWorkItem() { - var value = 0 - - let workItem = Executor.WorkItem { int in - value = int - } - - XCTAssertEqual(value, 0) - - workItem.execute(with: 1) - - XCTAssertEqual(value, 1) - } - - func testVoidWorkItemWithExecutor() { - var value = 0 - - let expectation = self.expectation(description: "testVoidWorkItemWithExecutor") - - let workItem = Executor.WorkItem { - value = 1 - expectation.fulfill() - } - - let executor = Executor.queue(.globalDefault()) - - executor.execute(workItem: workItem) - - waitForExpectations(timeout: 1) { _ in - XCTAssertEqual(value, 1) - } - } - - func testValueWorkItemWithExecutor() { - var value = 0 - - let expectation = self.expectation(description: "testValueWorkItemWithExecutor") - - let workItem = Executor.WorkItem { int in - value = int - expectation.fulfill() - } - - let executor = Executor.queue(.globalDefault()) - - executor.execute(workItem: workItem, with: 1) - - waitForExpectations(timeout: 1) { _ in - XCTAssertEqual(value, 1) - } - } - - func testCancelWorkItem() { - let queue = DispatchQueue(label: "testCancelWorkItem") - - var value = 0 - - let expectation = self.expectation(description: "testCancelWorkItem") - - let workItem = Executor.WorkItem { int in - value = int - } - - let executor = Executor.queue(queue) - - XCTAssertFalse(workItem.isCanceled) - - queue.suspend() - - executor.execute(workItem: workItem, with: 1) - workItem.cancel() - - queue.resume() - - workItem.cancel() - - queue.async(execute: expectation.fulfill) - - waitForExpectations(timeout: 1) { _ in - XCTAssertTrue(workItem.isCanceled) - XCTAssertEqual(value, 0) - } - } } diff --git a/Tests/SinkSignalTests.swift b/Tests/SinkSignalTests.swift index e8122e4..b41ad09 100644 --- a/Tests/SinkSignalTests.swift +++ b/Tests/SinkSignalTests.swift @@ -52,6 +52,33 @@ final class SinkSignalTests: XCTestCase { XCTAssertEqual(value, 1) } + func testImmediatelyDisposeSignal() { + let queue = DispatchQueue(label: "testImmediatelyDisposeSignal") + + let signal = Signal { send in + queue.async { + send(()) + } + return AnyDisposable {} + } + + queue.suspend() + + let disposable = signal.observe { + XCTFail() + } + + disposable.dispose() + + let expectation = self.expectation(description: "testImmediatelyDisposeSignal") + + queue.resume() + + queue.async(execute: expectation.fulfill) + + waitForExpectations(timeout: 1) { _ in } + } + func testDisposeOnObserving() { let sink = Sink() let signal = sink.signal diff --git a/VueFlux.podspec b/VueFlux.podspec index c7ed804..dc80369 100644 --- a/VueFlux.podspec +++ b/VueFlux.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'VueFlux' - spec.version = '1.3.1' + spec.version = '1.4.0' spec.author = { 'ra1028' => 'r.fe51028.r@gmail.com' } spec.homepage = 'https://github.com/ra1028/VueFlux' spec.summary = 'Unidirectional State Management for Swift - Inspired by Vuex and Flux' diff --git a/VueFlux.xcodeproj/project.pbxproj b/VueFlux.xcodeproj/project.pbxproj index ead6fcc..4d1f9b9 100644 --- a/VueFlux.xcodeproj/project.pbxproj +++ b/VueFlux.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ 6B41947A2045B72300C40218 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B4194792045B72300C40218 /* Lock.swift */; }; 6B41947B2045B72800C40218 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B4194792045B72300C40218 /* Lock.swift */; }; 6B41947D2045B7A500C40218 /* LockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B41947C2045B7A500C40218 /* LockTests.swift */; }; - 6B665528200AE8A000883C12 /* ExecutorWorkItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B665527200AE8A000883C12 /* ExecutorWorkItem.swift */; }; 6B730BC82045E9280059D851 /* AtomicBool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B730BC72045E9280059D851 /* AtomicBool.swift */; }; 6B730BC92045E9480059D851 /* AtomicBool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B730BC72045E9280059D851 /* AtomicBool.swift */; }; 6B730BCB2045E9620059D851 /* AtomicBoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B730BCA2045E9620059D851 /* AtomicBoolTests.swift */; }; @@ -40,6 +39,9 @@ 6BB30C4F2018E3CA00C52C76 /* Signal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB30C4E2018E3CA00C52C76 /* Signal.swift */; }; 6BB30C512018E3E700C52C76 /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB30C502018E3E700C52C76 /* Variable.swift */; }; 6BB30C532018E41600C52C76 /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB30C522018E41600C52C76 /* Constant.swift */; }; + 6BB9F56B20514317008C6C77 /* CancelableProcedureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB9F56A20514317008C6C77 /* CancelableProcedureTests.swift */; }; + 6BB9F56D20514DCA008C6C77 /* CancelableProcedure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB9F56C20514DCA008C6C77 /* CancelableProcedure.swift */; }; + 6BB9F56E20514DCA008C6C77 /* CancelableProcedure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB9F56C20514DCA008C6C77 /* CancelableProcedure.swift */; }; 6BC664F51FEC5ABF00BD74C8 /* AnyDisposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC664F41FEC5ABF00BD74C8 /* AnyDisposable.swift */; }; 6BC664F71FEC627E00BD74C8 /* SignalOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC664F61FEC627E00BD74C8 /* SignalOperators.swift */; }; 6BC665331FF032C500BD74C8 /* Executor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC665321FF032C500BD74C8 /* Executor.swift */; }; @@ -77,7 +79,6 @@ 6B14405820453C3D00E0B3FF /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; 6B4194792045B72300C40218 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = ""; }; 6B41947C2045B7A500C40218 /* LockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockTests.swift; sourceTree = ""; }; - 6B665527200AE8A000883C12 /* ExecutorWorkItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExecutorWorkItem.swift; sourceTree = ""; }; 6B730BC72045E9280059D851 /* AtomicBool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicBool.swift; sourceTree = ""; }; 6B730BCA2045E9620059D851 /* AtomicBoolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicBoolTests.swift; sourceTree = ""; }; 6B9E32811FA639DF000B24D4 /* VueFlux.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VueFlux.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -102,6 +103,8 @@ 6BB30C502018E3E700C52C76 /* Variable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Variable.swift; sourceTree = ""; }; 6BB30C522018E41600C52C76 /* Constant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constant.swift; sourceTree = ""; }; 6BB5BAF92044353A0009F779 /* Common.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; + 6BB9F56A20514317008C6C77 /* CancelableProcedureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelableProcedureTests.swift; sourceTree = ""; }; + 6BB9F56C20514DCA008C6C77 /* CancelableProcedure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelableProcedure.swift; sourceTree = ""; }; 6BC664F41FEC5ABF00BD74C8 /* AnyDisposable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyDisposable.swift; sourceTree = ""; }; 6BC664F61FEC627E00BD74C8 /* SignalOperators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalOperators.swift; sourceTree = ""; }; 6BC665321FF032C500BD74C8 /* Executor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Executor.swift; sourceTree = ""; }; @@ -153,8 +156,9 @@ isa = PBXGroup; children = ( 6B14405820453C3D00E0B3FF /* Storage.swift */, - 6B4194792045B72300C40218 /* Lock.swift */, + 6BB9F56C20514DCA008C6C77 /* CancelableProcedure.swift */, 6B730BC72045E9280059D851 /* AtomicBool.swift */, + 6B4194792045B72300C40218 /* Lock.swift */, ); path = CommonInternal; sourceTree = ""; @@ -197,8 +201,9 @@ 6BA27BCB1FB8CD3100809472 /* DisposableTests.swift */, 6BA27BCE1FB8CE1A00809472 /* DisposableScopeTests.swift */, 6BA27BD31FB8CFE600809472 /* StorageTests.swift */, - 6B41947C2045B7A500C40218 /* LockTests.swift */, + 6BB9F56A20514317008C6C77 /* CancelableProcedureTests.swift */, 6B730BCA2045E9620059D851 /* AtomicBoolTests.swift */, + 6B41947C2045B7A500C40218 /* LockTests.swift */, 6B9E329F1FA63B07000B24D4 /* Info.plist */, ); path = Tests; @@ -209,7 +214,6 @@ children = ( 6B9EB21C1FCB14BF009F0659 /* VueFlux.swift */, 6BC665321FF032C500BD74C8 /* Executor.swift */, - 6B665527200AE8A000883C12 /* ExecutorWorkItem.swift */, 6B9EB23D1FCB1CFB009F0659 /* AtomicReference.swift */, 6B9EB2141FCB14BF009F0659 /* Internal */, 6B9EB2131FCB14BF009F0659 /* Info.plist */, @@ -441,10 +445,10 @@ buildActionMask = 2147483647; files = ( 6B730BC82045E9280059D851 /* AtomicBool.swift in Sources */, + 6BB9F56E20514DCA008C6C77 /* CancelableProcedure.swift in Sources */, 6B9EB21D1FCB14ED009F0659 /* Dispatcher.swift in Sources */, 6B14405920453C4300E0B3FF /* Storage.swift in Sources */, 6B9EB21E1FCB14ED009F0659 /* DispatcherContext.swift in Sources */, - 6B665528200AE8A000883C12 /* ExecutorWorkItem.swift in Sources */, 6B9EB2241FCB14ED009F0659 /* VueFlux.swift in Sources */, 6B9EB23F1FCB1CFB009F0659 /* AtomicReference.swift in Sources */, 6B41947A2045B72300C40218 /* Lock.swift in Sources */, @@ -459,6 +463,7 @@ 6B41947D2045B7A500C40218 /* LockTests.swift in Sources */, 6B9EB2441FCB203B009F0659 /* VueFluxTests.swift in Sources */, 6B9EB2451FCB203B009F0659 /* ExecutorTests.swift in Sources */, + 6BB9F56B20514317008C6C77 /* CancelableProcedureTests.swift in Sources */, 6B9EB2571FCB2FEE009F0659 /* SinkSignalTests.swift in Sources */, 6B730BCB2045E9620059D851 /* AtomicBoolTests.swift in Sources */, 6B9EB2461FCB203B009F0659 /* DisposableTests.swift in Sources */, @@ -482,6 +487,7 @@ 6BB30C4F2018E3CA00C52C76 /* Signal.swift in Sources */, 6BC664F71FEC627E00BD74C8 /* SignalOperators.swift in Sources */, 6BB30C4D2018E3AC00C52C76 /* Sink.swift in Sources */, + 6BB9F56D20514DCA008C6C77 /* CancelableProcedure.swift in Sources */, 6BB30C512018E3E700C52C76 /* Variable.swift in Sources */, 6BC664F51FEC5ABF00BD74C8 /* AnyDisposable.swift in Sources */, 6B730BC92045E9480059D851 /* AtomicBool.swift in Sources */, diff --git a/VueFlux/Executor.swift b/VueFlux/Executor.swift index 64ee445..f2444a9 100644 --- a/VueFlux/Executor.swift +++ b/VueFlux/Executor.swift @@ -38,23 +38,6 @@ public struct Executor { public func execute(_ function: @escaping () -> Void) { context(function) } - - /// Execute an arbitrary work item. - /// - /// - Parameters: - /// - workItem: A work item to be execute. - /// - value: A value that pass to work item. - public func execute(workItem: WorkItem, with value: @autoclosure @escaping () -> Value) { - execute { workItem.execute(with: value()) } - } - - /// Execute an arbitrary work item. - /// - /// - Parameters: - /// - workItem: A work item to be execute. - public func execute(workItem: WorkItem) { - execute { workItem.execute() } - } } private extension Executor { diff --git a/VueFlux/ExecutorWorkItem.swift b/VueFlux/ExecutorWorkItem.swift deleted file mode 100644 index fcb67b2..0000000 --- a/VueFlux/ExecutorWorkItem.swift +++ /dev/null @@ -1,45 +0,0 @@ -public extension Executor { - /// Encapsulates the function to execute by Executor. - /// Also be able to cancel its execution. - public final class WorkItem { - /// A Bool value indicating whether canceled. - public var isCanceled: Bool { - return _isCanceled.value - } - - private let _isCanceled: AtomicBool = false - private var _execute: ((Value) -> Void)? - - /// Create with an arbitrary function. - /// - /// - Parameters: - /// - execute: A function to be executed by calling `execute(with:)` until canceled. - public init(_ execute: @escaping (Value) -> Void) { - _execute = execute - } - - /// Synchronously execute the specified function. - /// - /// - Parameters: - /// - value: A value to be pass to specified function. - public func execute(with value: @autoclosure () -> Value) { - guard !isCanceled, let execute = _execute else { return } - execute(value()) - } - - /// Cancel the specified function. - /// Cancellation does not affect any execution of the function that is already in progress. - public func cancel() { - guard _isCanceled.compareAndSwapBarrier(old: false, new: true) else { return } - _execute = nil - } - } -} - -public extension Executor.WorkItem where Value == Void { - /// Synchronously execute the specified function. - @inline(__always) - public func execute() { - self.execute(with: ()) - } -} diff --git a/VueFlux/VueFlux.swift b/VueFlux/VueFlux.swift index f4fb6fc..96c8732 100644 --- a/VueFlux/VueFlux.swift +++ b/VueFlux/VueFlux.swift @@ -3,7 +3,7 @@ open class Store { private let dispatcher = Dispatcher() private let sharedDispatcher = Dispatcher.shared - private let commitWorkItem: Executor.WorkItem + private let commitProcedure: CancelableProcedure private let dispatcherKey: Dispatcher.Observers.Key private let sharedDispatcherKey: Dispatcher.Observers.Key @@ -26,15 +26,15 @@ open class Store { /// - mutations: A mutations for mutates the state. /// - executor: An executor to dispatch actions on. public init(state: State, mutations: State.Mutations, executor: Executor) { - let commitWorkItem = Executor.WorkItem { action in + let commitProcedure = CancelableProcedure { action in mutations.commit(action: action, state: state) } let commit: (State.Action) -> Void = { action in - executor.execute(workItem: commitWorkItem, with: action) + executor.execute { commitProcedure.execute(with: action) } } - self.commitWorkItem = commitWorkItem + self.commitProcedure = commitProcedure actions = .init(dispatcher: dispatcher) computed = .init(state: state) @@ -44,7 +44,7 @@ open class Store { } deinit { - commitWorkItem.cancel() + commitProcedure.cancel() dispatcher.unsubscribe(for: dispatcherKey) sharedDispatcher.unsubscribe(for: sharedDispatcherKey) } diff --git a/VueFluxReactive.podspec b/VueFluxReactive.podspec index 468a707..60e0baf 100644 --- a/VueFluxReactive.podspec +++ b/VueFluxReactive.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |spec| spec.name = 'VueFluxReactive' - spec.version = '1.3.1' + spec.version = '1.4.0' spec.author = { 'ra1028' => 'r.fe51028.r@gmail.com' } spec.homepage = 'https://github.com/ra1028/VueFlux' spec.summary = 'Reactive system for VueFlux architecture in Swift' spec.source = { :git => 'https://github.com/ra1028/VueFlux.git', :tag => spec.version.to_s } spec.license = { :type => 'MIT', :file => 'LICENSE' } spec.source_files = 'VueFluxReactive/**/*.swift', 'CommonInternal/**/*.swift' - spec.dependency 'VueFlux', '~> 1.3.1' + spec.dependency 'VueFlux', '~> 1.4.0' spec.requires_arc = true spec.osx.deployment_target = '10.9' spec.ios.deployment_target = '9.0' diff --git a/VueFluxReactive/AnyDisposable.swift b/VueFluxReactive/AnyDisposable.swift index 351b5e5..d2d9679 100644 --- a/VueFluxReactive/AnyDisposable.swift +++ b/VueFluxReactive/AnyDisposable.swift @@ -10,7 +10,7 @@ public final class AnyDisposable: Disposable { private let _isDisposed: AtomicBool = false private var _dispose: (() -> Void)? - /// Create with dispose function. + /// Initialize with dispose function. /// /// - Parameters: /// - dispose: A function to run when disposed. @@ -18,6 +18,11 @@ public final class AnyDisposable: Disposable { _dispose = dispose } + /// Initialize a nop disposable. + public init() { + _dispose = nil + } + /// Dispose if not already been disposed. public func dispose() { guard _isDisposed.compareAndSwapBarrier(old: false, new: true) else { return } diff --git a/VueFluxReactive/Signal.swift b/VueFluxReactive/Signal.swift index 78c7681..e04548e 100644 --- a/VueFluxReactive/Signal.swift +++ b/VueFluxReactive/Signal.swift @@ -22,7 +22,15 @@ public struct Signal { /// - Returns: A disposable to unregister given observer. @discardableResult public func observe(_ observer: @escaping (Value) -> Void) -> Disposable { - return producer(observer) + let observerProcedure = CancelableProcedure(observer) + let disposable = producer { value in + observerProcedure.execute(with: value) + } + + return AnyDisposable { + observerProcedure.cancel() + disposable.dispose() + } } /// observe the values to the given observer during during scope of specified object. diff --git a/VueFluxReactive/SignalOperators.swift b/VueFluxReactive/SignalOperators.swift index e096512..395b992 100644 --- a/VueFluxReactive/SignalOperators.swift +++ b/VueFluxReactive/SignalOperators.swift @@ -8,10 +8,8 @@ public extension Signal { /// /// - Returns: A signal to be receives new values. public func map(_ transform: @escaping (Value) -> T) -> Signal { - return .init { send in - self.observe { value in - send(transform(value)) - } + return operated { value, send in + send(transform(value)) } } @@ -22,16 +20,17 @@ public extension Signal { /// /// - returns: A signal that will forward values on given executor. public func observe(on executor: Executor) -> Signal { + return operated { value, send in + executor.execute { send(value) } + } + } +} + +private extension Signal { + func operated(_ operation: @escaping (Value, @escaping (T) -> Void) -> Void) -> Signal { return .init { send in - let workItem = Executor.WorkItem(send) - - let disposable = self.observe { value in - executor.execute(workItem: workItem, with: value) - } - - return AnyDisposable { - workItem.cancel() - disposable.dispose() + self.observe { value in + operation(value, send) } } }