Skip to content

Commit

Permalink
Allow .onReceive to accept any type of AsyncSequence. (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
BrentMifsud authored Oct 2, 2022
1 parent dc258d0 commit 4625d50
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 13 deletions.
67 changes: 67 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/AsyncValue.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AsyncValue"
BuildableName = "AsyncValue"
BlueprintName = "AsyncValue"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AsyncValue"
BuildableName = "AsyncValue"
BlueprintName = "AsyncValue"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
2 changes: 0 additions & 2 deletions AsyncValueTestApp/TestPlans/AllTests.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@
},
"testTargets" : [
{
"parallelizable" : true,
"target" : {
"containerPath" : "container:AsyncValueTestApp.xcodeproj",
"identifier" : "2AA580C628C2FFD7006EE900",
"name" : "AsyncValueTestAppUITests"
}
},
{
"parallelizable" : true,
"target" : {
"containerPath" : "container:..",
"identifier" : "AsyncValueTests",
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
55 changes: 46 additions & 9 deletions Sources/AsyncValue/View+AsyncValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,69 @@
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 {
/// Text("Hello World!")
/// .onReceive(myService.$myValue) { value in
/// print("The value changed to: \(value)")
/// }
/// .onReceive(myService.myStream) { newValue in
/// print("My stream value changed to: \(newValue)")
/// }
/// }
/// }
///
/// class MyService: ObservableObject {
/// @AsyncValue var myValue: String = "Test" {
/// willSet { objectWillChange.send() }
/// }
///
/// lazy var myStream: AsyncStream<Int> = {
/// // initialize and return some async stream here
/// }()
/// }
/// ```
@ViewBuilder func onReceive<Value>(
_ stream: AsyncStream<Value>,
perform handler: @escaping (Value) async -> Void
@ViewBuilder func onReceive<Sequence: AsyncSequence>(
_ 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 {
var task: Task<Void, Never>? = nil

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)
}
}
}
Expand All @@ -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

0 comments on commit 4625d50

Please sign in to comment.