Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions RIBs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
AFB7D4031FC81C8F00045D2B /* Foundation+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF82F6731FC81B5F006DF7BC /* Foundation+ExtensionsTests.swift */; };
AFB7D4051FC81D6100045D2B /* LaunchRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFB7D4041FC81D6100045D2B /* LaunchRouterTests.swift */; };
BF5FC0F122808377004235F1 /* RxRelay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5FC0F022808377004235F1 /* RxRelay.framework */; };
C0A100012B10000100A10001 /* Observable+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A100032B10000100A10001 /* Observable+Async.swift */; };
C0A100082B10000200A10001 /* Task+RIBs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A1000A2B10000200A10001 /* Task+RIBs.swift */; };
C0A101022B10001000A10001 /* ObservableConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A101012B10001000A10001 /* ObservableConcurrencyTests.swift */; };
C0A101042B10001000A10001 /* InteractorConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A101032B10001000A10001 /* InteractorConcurrencyTests.swift */; };
C0A101062B10001000A10001 /* WorkerConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A101052B10001000A10001 /* WorkerConcurrencyTests.swift */; };
C0A1010C2B10001000A10001 /* WorkflowConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A1010B2B10001000A10001 /* WorkflowConcurrencyTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -75,6 +81,12 @@
AF9966B11FC40D7E00CAEAA2 /* RxSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxSwift.framework; path = ../Carthage/Build/iOS/RxSwift.framework; sourceTree = "<group>"; };
AFB7D4041FC81D6100045D2B /* LaunchRouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchRouterTests.swift; sourceTree = "<group>"; };
BF5FC0F022808377004235F1 /* RxRelay.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxRelay.framework; path = ../Carthage/Build/iOS/RxRelay.framework; sourceTree = "<group>"; };
C0A100032B10000100A10001 /* Observable+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+Async.swift"; sourceTree = "<group>"; };
C0A101012B10001000A10001 /* ObservableConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableConcurrencyTests.swift; sourceTree = "<group>"; };
C0A101032B10001000A10001 /* InteractorConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractorConcurrencyTests.swift; sourceTree = "<group>"; };
C0A101052B10001000A10001 /* WorkerConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkerConcurrencyTests.swift; sourceTree = "<group>"; };
C0A1010B2B10001000A10001 /* WorkflowConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkflowConcurrencyTests.swift; sourceTree = "<group>"; };
C0A1000A2B10000200A10001 /* Task+RIBs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+RIBs.swift"; sourceTree = "<group>"; };
E8E789432378AD000043E59E /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Package.swift; path = ../Package.swift; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -110,6 +122,7 @@
413177271F8EEFEF005F08F0 /* Router.swift */,
413177241F8EEFEF005F08F0 /* ViewableRouter.swift */,
413177281F8EEFEF005F08F0 /* ViewControllable.swift */,
C0A100002B10000100A10001 /* Concurrency */,
418C17551F97DB0E003C03F7 /* DI */,
4131773B1F8EF981005F08F0 /* Extensions */,
413177361F8EF70A005F08F0 /* LeakDetector */,
Expand All @@ -119,6 +132,15 @@
path = Classes;
sourceTree = "<group>";
};
C0A100002B10000100A10001 /* Concurrency */ = {
isa = PBXGroup;
children = (
C0A100032B10000100A10001 /* Observable+Async.swift */,
C0A1000A2B10000200A10001 /* Task+RIBs.swift */,
);
path = Concurrency;
sourceTree = "<group>";
};
413177201F8EEFEF005F08F0 /* Workflow */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -195,6 +217,7 @@
isa = PBXGroup;
children = (
8B9882F21F86E1CF00ABE009 /* Info.plist */,
C0A1000B2B10000300A10001 /* Concurrency */,
AFB7D4041FC81D6100045D2B /* LaunchRouterTests.swift */,
AF90B40B1FBA157700920384 /* Mocks.swift */,
AF90B4091FBA14DB00920384 /* RouterTests.swift */,
Expand All @@ -206,6 +229,17 @@
path = RIBsTests;
sourceTree = "<group>";
};
C0A1000B2B10000300A10001 /* Concurrency */ = {
isa = PBXGroup;
children = (
C0A101032B10001000A10001 /* InteractorConcurrencyTests.swift */,
C0A101012B10001000A10001 /* ObservableConcurrencyTests.swift */,
C0A101052B10001000A10001 /* WorkerConcurrencyTests.swift */,
C0A1010B2B10001000A10001 /* WorkflowConcurrencyTests.swift */,
);
path = Concurrency;
sourceTree = "<group>";
};
AF5101421FBBA64A009C0DB3 /* Frameworks */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -392,6 +426,8 @@
4131772D1F8EF5FF005F08F0 /* Interactor.swift in Sources */,
413177321F8EF5FF005F08F0 /* ViewableRouter.swift in Sources */,
413177351F8EF605005F08F0 /* Workflow.swift in Sources */,
C0A100012B10000100A10001 /* Observable+Async.swift in Sources */,
C0A100082B10000200A10001 /* Task+RIBs.swift in Sources */,
4131772F1F8EF5FF005F08F0 /* PresentableInteractor.swift in Sources */,
4131772C1F8EF5FF005F08F0 /* Builder.swift in Sources */,
413177331F8EF5FF005F08F0 /* ViewControllable.swift in Sources */,
Expand All @@ -404,6 +440,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C0A101042B10001000A10001 /* InteractorConcurrencyTests.swift in Sources */,
C0A101022B10001000A10001 /* ObservableConcurrencyTests.swift in Sources */,
C0A101062B10001000A10001 /* WorkerConcurrencyTests.swift in Sources */,
C0A1010C2B10001000A10001 /* WorkflowConcurrencyTests.swift in Sources */,
AFB7D4031FC81C8F00045D2B /* Foundation+ExtensionsTests.swift in Sources */,
AF90B4111FBA185E00920384 /* ComponentTests.swift in Sources */,
AFB7D4051FC81D6100045D2B /* LaunchRouterTests.swift in Sources */,
Expand Down
48 changes: 48 additions & 0 deletions RIBs/Classes/Concurrency/Observable+Async.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// Copyright (c) 2017. Uber Technologies
//
// 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.
//

#if swift(>=5.6) && canImport(_Concurrency)

import RxSwift

public extension PrimitiveSequenceType where Trait == SingleTrait {

/// Create a cold single that runs the given async work on subscription.
static func fromAsync(_ work: @escaping () async throws -> Element) -> Single<Element> {
return .create { observer in
let task = Task {
do {
observer(.success(try await work()))
} catch {
observer(.failure(error))
}
}

return Disposables.create {
task.cancel()
}
}
}
}

public extension ObservableType {

/// Create a cold observable that runs the given async work on subscription.
static func fromAsync(_ work: @escaping () async throws -> Element) -> Observable<Element> {
return Single<Element>.fromAsync(work).asObservable()
}
}
#endif
53 changes: 53 additions & 0 deletions RIBs/Classes/Concurrency/Task+RIBs.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// Copyright (c) 2017. Uber Technologies
//
// 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.
//

#if swift(>=5.6) && canImport(_Concurrency)

import RxSwift

public extension Task {

/// Cancel this task when the given interactor deactivates.
@discardableResult
func cancelOnDeactivate(interactor: Interactor) -> Task<Success, Failure> {
Disposables.create {
self.cancel()
}
.disposeOnDeactivate(interactor: interactor)
return self
}

/// Cancel this task when the given worker stops.
@discardableResult
func cancelOnStop(_ worker: Worker) -> Task<Success, Failure> {
Disposables.create {
self.cancel()
}
.disposeOnStop(worker)
return self
}

/// Cancel this task when the given workflow is disposed.
@discardableResult
func cancel<ActionableItemType>(with workflow: Workflow<ActionableItemType>) -> Task<Success, Failure> {
Disposables.create {
self.cancel()
}
.disposeWith(workflow: workflow)
return self
}
}
#endif
29 changes: 28 additions & 1 deletion RIBs/Classes/Workflow/Workflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ open class Workflow<ActionableItemType> {
}
}

/// Execute the given async closure as the root step.
///
/// - parameter onStep: The async closure to execute for the root step.
/// - returns: The next step.
public final func onAsyncStep<NextActionableItemType, NextValueType>(_ onStep: @escaping (ActionableItemType) async throws -> (NextActionableItemType, NextValueType)) -> Step<ActionableItemType, NextActionableItemType, NextValueType> {
return self.onStep { actionableItem in
Single.fromAsync {
try await onStep(actionableItem)
}
.asObservable()
}
}

/// Subscribe and start the `Workflow` sequence.
///
/// - parameter actionableItem: The initial actionable item for the first step.
Expand Down Expand Up @@ -146,6 +159,19 @@ open class Step<WorkflowActionableItemType, ActionableItemType, ValueType> {
return Step<WorkflowActionableItemType, NextActionableItemType, NextValueType>(workflow: workflow, observable: confinedNextStep)
}

/// Executes the given async closure for this step.
///
/// - parameter onStep: The async closure to execute for the `Step`.
/// - returns: The next step.
public final func onAsyncStep<NextActionableItemType, NextValueType>(_ onStep: @escaping (ActionableItemType, ValueType) async throws -> (NextActionableItemType, NextValueType)) -> Step<WorkflowActionableItemType, NextActionableItemType, NextValueType> {
return self.onStep { actionableItem, value in
Single.fromAsync {
try await onStep(actionableItem, value)
}
.asObservable()
}
}

/// Executes the given closure when the `Step` produces an error.
///
/// - parameter onError: The closure to execute when an error occurs.
Expand Down Expand Up @@ -175,9 +201,10 @@ open class Step<WorkflowActionableItemType, ActionableItemType, ValueType> {
public final func asObservable() -> Observable<(ActionableItemType, ValueType)> {
return observable
}

}

/// `Workflow` related obervable extensions.

public extension ObservableType {

/// Fork the step from this obervable.
Expand Down
81 changes: 81 additions & 0 deletions RIBsTests/Concurrency/InteractorConcurrencyTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// Copyright (c) 2017. Uber Technologies
//
// 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 XCTest
@testable import RIBs

final class InteractorConcurrencyTests: XCTestCase {

func test_taskCancelOnDeactivate_cancelsTaskWhenInteractorDeactivates() async {
let interactor = Interactor()
interactor.activate()
let taskCancelled = expectation(description: "Task cancelled")

let task = Task {
while !Task.isCancelled {
await Task.yield()
}
taskCancelled.fulfill()
}
.cancelOnDeactivate(interactor: interactor)
XCTAssertFalse(task.isCancelled)

interactor.deactivate()
await fulfillment(of: [taskCancelled], timeout: 1)

XCTAssertTrue(task.isCancelled)
}

func test_taskCancelOnDeactivate_cancelsImmediatelyWhenInteractorIsInactive() async {
let interactor = Interactor()
let taskCancelled = expectation(description: "Task cancelled")

let task = Task {
while !Task.isCancelled {
await Task.yield()
}
taskCancelled.fulfill()
}
.cancelOnDeactivate(interactor: interactor)

await fulfillment(of: [taskCancelled], timeout: 1)
XCTAssertTrue(task.isCancelled)
}

func test_throwingTaskCancelOnDeactivate_cancelsTaskWhenInteractorDeactivates() async {
let interactor = Interactor()
interactor.activate()

let task = Task {
try await Task.sleep(nanoseconds: 10_000_000_000)
}
.cancelOnDeactivate(interactor: interactor)
XCTAssertFalse(task.isCancelled)

interactor.deactivate()

do {
try await task.value
XCTFail("Expected task to throw CancellationError")
} catch is CancellationError {
// Expected.
} catch {
XCTFail("Unexpected error: \(error)")
}

XCTAssertTrue(task.isCancelled)
}
}
Loading
Loading