From 4625d50a233fcfa9e581b73faa579b31645a974c Mon Sep 17 00:00:00 2001 From: Brent Mifsud <18543934+BrentMifsud@users.noreply.github.com> Date: Sun, 2 Oct 2022 19:10:03 -0400 Subject: [PATCH] Allow .onReceive to accept any type of AsyncSequence. (#8) --- .../xcschemes/AsyncValue.xcscheme | 67 +++++++++++++++++++ .../TestPlans/AllTests.xctestplan | 2 - Package.swift | 2 +- README.md | 2 +- Sources/AsyncValue/View+AsyncValue.swift | 55 ++++++++++++--- 5 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/AsyncValue.xcscheme diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AsyncValue.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AsyncValue.xcscheme new file mode 100644 index 0000000..8fa57d0 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/AsyncValue.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AsyncValueTestApp/TestPlans/AllTests.xctestplan b/AsyncValueTestApp/TestPlans/AllTests.xctestplan index 1fa55f8..984556e 100644 --- a/AsyncValueTestApp/TestPlans/AllTests.xctestplan +++ b/AsyncValueTestApp/TestPlans/AllTests.xctestplan @@ -16,7 +16,6 @@ }, "testTargets" : [ { - "parallelizable" : true, "target" : { "containerPath" : "container:AsyncValueTestApp.xcodeproj", "identifier" : "2AA580C628C2FFD7006EE900", @@ -24,7 +23,6 @@ } }, { - "parallelizable" : true, "target" : { "containerPath" : "container:..", "identifier" : "AsyncValueTests", diff --git a/Package.swift b/Package.swift index 65d482c..e551d9a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.5 +// swift-tools-version: 5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/README.md b/README.md index 02ac0f2..2ac5166 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ class MyObservableObject: ObservableObject { } ``` -There is also an `.onRecieve(stream:perform:)` view modifier that allows you to respond to changes from an @AsyncValue +There is also an `.onRecieve(sequence:perform:)` view modifier that allows you to respond to changes from any AsyncSequence. ```swift struct MyView: View { diff --git a/Sources/AsyncValue/View+AsyncValue.swift b/Sources/AsyncValue/View+AsyncValue.swift index 621f6b9..41ea342 100644 --- a/Sources/AsyncValue/View+AsyncValue.swift +++ b/Sources/AsyncValue/View+AsyncValue.swift @@ -10,14 +10,17 @@ import SwiftUI public extension View { - /// Adds a modifier for this view that fires an action when a specific `AsyncValue` changes. + /// Adds a modifier for this view that fires an action when a specific `AsyncSequence` yields new values. /// - Parameters: - /// - stream: the async stream to recieve updates from + /// - sequence: the async sequence to recieve updates from /// - handler: handler for new values /// - Returns: some View /// /// This view modifier works very similarly to `.onReceive(publisher:perform:)` and `.onChange(value:perform:)` /// + /// A warning will be printed to the console if the provided `AsyncSequence` throws an error. And no new values will be returned. + /// As such, it is recommended that `AsyncStream` is used rather than a custom `AsyncSequence` implementation. + /// /// ```swift /// struct MyView: View { /// var body: some View { @@ -25,6 +28,9 @@ public extension View { /// .onReceive(myService.$myValue) { value in /// print("The value changed to: \(value)") /// } + /// .onReceive(myService.myStream) { newValue in + /// print("My stream value changed to: \(newValue)") + /// } /// } /// } /// @@ -32,16 +38,26 @@ public extension View { /// @AsyncValue var myValue: String = "Test" { /// willSet { objectWillChange.send() } /// } + /// + /// lazy var myStream: AsyncStream = { + /// // initialize and return some async stream here + /// }() /// } /// ``` - @ViewBuilder func onReceive( - _ stream: AsyncStream, - perform handler: @escaping (Value) async -> Void + @ViewBuilder func onReceive( + _ sequence: Sequence, + perform handler: @escaping (Sequence.Element) async -> Void ) -> some View { if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { task { - for await value in stream { - await handler(value) + do { + for try await value in sequence { + await handler(value) + } + } catch is CancellationError { + // cancellation errors are valid and we can ignore them. + } catch { + Self.printErrorWarning(error: error, sequence: sequence, function: #function, line: #line, file: #file) } } } else { @@ -49,8 +65,14 @@ public extension View { onAppear { task = Task { - for await value in stream { - await handler(value) + do { + for try await value in sequence { + await handler(value) + } + } catch is CancellationError { + // cancellation errors are valid and we can ignore them. + } catch { + Self.printErrorWarning(error: error, sequence: sequence, function: #function, line: #line, file: #file) } } } @@ -59,6 +81,21 @@ public extension View { } } } + + private static func printErrorWarning( + error: Error, + sequence: any AsyncSequence, + function: StaticString = #function, + line: Int = #line, + file: StaticString = #file + ) { + print(""" + [AsyncValue] - warning: usage of .onReceive(sequence:handler:) with unhandled throwing sequence at: \(file) \(function) \(line) + The AsyncSequence throwing the error: \(type(of: sequence)) + The error has been caught here to help with debugging purposes: \(String(describing: error)) + Please handle any errors thrown in your custom AsyncSequence implementation, or try using AsyncStream instead. + """) + } } #endif