diff --git a/Documentation/SwiftConcurrency.md b/Documentation/SwiftConcurrency.md index e4e7549c1..d0b92990b 100644 --- a/Documentation/SwiftConcurrency.md +++ b/Documentation/SwiftConcurrency.md @@ -63,3 +63,17 @@ stream.asObservable() onError: { ... } ) ``` + +### Wrapping an `async` result as a `Single` + +If you already have an async piece of work that returns a single result you wish to await, you can bridge it back to the Rx wordl by using `Single.create`, a special overload which takes an `async throws` closure where you can simply await your async work: + +```swift +func doIncredibleWork() async throws -> AmazingRespones { + ... +} + +let single = Single.create { + try await doIncredibleWork() +} // Single +``` \ No newline at end of file diff --git a/Rx.xcodeproj/project.pbxproj b/Rx.xcodeproj/project.pbxproj index 7206acf40..cbfcc16ef 100644 --- a/Rx.xcodeproj/project.pbxproj +++ b/Rx.xcodeproj/project.pbxproj @@ -1071,7 +1071,7 @@ C8093CBF1B8A72BE0088E94D /* PublishSubject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PublishSubject.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; C8093CC01B8A72BE0088E94D /* ReplaySubject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ReplaySubject.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; C8093CC11B8A72BE0088E94D /* SubjectType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubjectType.swift; sourceTree = ""; }; - C8093E8B1B8A732E0088E94D /* DelegateProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DelegateProxy.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + C8093E8B1B8A732E0088E94D /* DelegateProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DelegateProxy.swift; sourceTree = ""; }; C8093E8C1B8A732E0088E94D /* DelegateProxyType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DelegateProxyType.swift; sourceTree = ""; }; C8093E9C1B8A732E0088E94D /* RxTarget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxTarget.swift; sourceTree = ""; }; C8093E9D1B8A732E0088E94D /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; diff --git a/RxSwift/Traits/PrimitiveSequence/PrimitiveSequence+Concurrency.swift b/RxSwift/Traits/PrimitiveSequence/PrimitiveSequence+Concurrency.swift index 4c520f71b..271df34d4 100644 --- a/RxSwift/Traits/PrimitiveSequence/PrimitiveSequence+Concurrency.swift +++ b/RxSwift/Traits/PrimitiveSequence/PrimitiveSequence+Concurrency.swift @@ -11,6 +11,38 @@ import Foundation #if swift(>=5.6) && canImport(_Concurrency) && !os(Linux) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension PrimitiveSequenceType where Trait == SingleTrait { + /** + Creates an `Single` from the result of an asynchronous operation + + - seealso: [create operator on reactivex.io](http://reactivex.io/documentation/operators/create.html) + + - parameter work: An `async` closure expected to return an element of type `Element` + + - returns: A `Single` of the `async` closure's element type + */ + @_disfavoredOverload + static func create( + detached: Bool = false, + priority: TaskPriority? = nil, + work: @Sendable @escaping () async throws -> Element + ) -> PrimitiveSequence { + .create { single in + let operation: () async throws -> Void = { + await single( + Result { try await work() } + ) + } + + let task = if detached { + Task.detached(priority: priority, operation: operation) + } else { + Task(priority: priority, operation: operation) + } + + return Disposables.create { task.cancel() } + } + } + /// Allows awaiting the success or failure of this `Single` /// asynchronously via Swift's concurrency features (`async/await`) /// @@ -161,4 +193,16 @@ public extension PrimitiveSequenceType where Trait == CompletableTrait, Element } } } + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension Result where Failure == Swift.Error { + @_disfavoredOverload + init(catching body: () async throws -> Success) async { + do { + self = try await .success(body()) + } catch { + self = .failure(error) + } + } +} #endif diff --git a/Tests/RxSwiftTests/PrimitiveSequence+ConcurrencyTests.swift b/Tests/RxSwiftTests/PrimitiveSequence+ConcurrencyTests.swift index b9efb325b..8ecd85647 100644 --- a/Tests/RxSwiftTests/PrimitiveSequence+ConcurrencyTests.swift +++ b/Tests/RxSwiftTests/PrimitiveSequence+ConcurrencyTests.swift @@ -11,6 +11,7 @@ import Dispatch import RxSwift import XCTest import RxTest +import RxBlocking @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) class PrimitiveSequenceConcurrencyTests: RxTest { @@ -72,6 +73,19 @@ extension PrimitiveSequenceConcurrencyTests { task.cancel() } + func testCreateSingleFromAsync() { + let randomResult = Int.random(in: 100...100000) + let work: () async throws -> Int = { randomResult } + + let single = Single.create { + try await work() + } + + XCTAssertEqual( + try! single.toBlocking().toArray(), + [randomResult] + ) + } } // MARK: - Maybe