Announcement: Concurrency Beta #1186
Replies: 9 comments 29 replies
-
In my testing it appears that the default timeout of one millisecond isn't enough for things like |
Beta Was this translation helpful? Give feedback.
-
The following Gist demonstrates that the phenomenon can be spotted by a So the idea is to discuss whether the problem lies in the Reducer { state, action, environment in
func updateDescription(_ inout State) {…}
switch action {
case .binding:
updateDescription(&state)
return .none
case .updateDescription:
updateDescription(&state)
return .none
}
} Hopefully, reducers as protocol could help formalizing this kind of lego construction, where we could imagine ultra-specialized Reducers components of larger Reducers, processing only one kind of information/action. Another standard example of action forwarding is for dismissing some presented modal whenever the user selects a value: you can either set the state's modal properties/flag to This issue wasn't really expressing until now, but if one starts to use more and more It also poses the question of the necessity or not to have synchronous |
Beta Was this translation helpful? Give feedback.
-
Are yall open to Linux support in the beta? Will it be possible to get away with no reliance on Combine, or not quite yet? My use case is that I'm working on an iOS/macOS app, but I'd be able to run our unit test CI on Linux boxes if we no longer needed to use Combine, which would be 10x cheaper. |
Beta Was this translation helpful? Give feedback.
-
Hey everyone, we just opened a draft PR (#1189) of our ongoing work bringing concurrency to the library. We will be making smaller, more focused PR's against that branch so that it is clear how we are evolving things for the next few weeks. |
Beta Was this translation helpful? Give feedback.
-
In the current |
Beta Was this translation helpful? Give feedback.
-
In some reducer, I'm updating some CoreData store using a Should I can understand why it could matter in terms of future-proofing in any case Swift gets typed throws and the library goes back to |
Beta Was this translation helpful? Give feedback.
-
As I'm converting my project, I'm encountering many WithViewStore(store) { viewStore in
Text("Hello \(viewStore.name)!")
.task { await viewStore.send(.task).finish() }
} As this pattern happens a lot, I'm wondering if we could improve the situation with a little sugar. For example, as we know we'll need a WithViewStore(store) { viewStore in
Text("Hello \(viewStore.name)!")
}.task(action: .task) I've also experimented with WithViewStore(store, task: .task) { viewStore in
Text("Hello \(viewStore.name)!")
} Another approach could directly use VStack {
DetailView(store.scope(…))
}.task(store: store, action: .task) Thus, we could call If the " What do you think about this? |
Beta Was this translation helpful? Give feedback.
-
Does Without it, adding a (Very possible I'm doing something wrong, in which case, disregard!) |
Beta Was this translation helpful? Give feedback.
-
Hello everyone, today we have officially released 0.39.0 of the library which makes all of these async tools available in the library, and ends the beta period of this feature. Thanks to everyone that provided feedback and helped catch bugs! |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Hey everyone! As we kick off our series of episodes on "Async Composable Architecture," we'd like to invite users of the library to test our upcoming release more formally. We’ve been working on these tools for many, many, many months, and are excited that they are finally coming together! We hope to gather community feedback to make them even better for release.
The episodes will dive deep into the motivation and basic implementations of many new async features, but we'll do our best to give an overview of the additions and changes below. Most of these changes should be fully backwards-compatible, with a few minor exceptions. All deprecations are "soft" for now and should not produce noisy warnings.
Changes to reducer effects
The
Effect
type has been tightly coupled to the Combine framework for a long time now: it conforms toPublisher
and we have expected library users to interact with it as such. While Combine suited us when TCA first launched, it is a complex framework that SwiftUI mostly hides behind the scenes, and is a learning hurdle for library users. It is also closed-source, which prevents TCA from being used on non-Apple platforms.In our upcoming release we will begin to loosen our dependence on Combine by nudging library users toward a smaller collection of async/await endpoints on the
Effect
type. We believe that it is possible to define all of your application's effects in terms of these endpoints, and you can even completely avoid interacting withEffect
as a Combine publisher. In the future, we hope to deprecate our dependency on Combine entirely and instead just offer the following surface area:Effect.none
This endpoint is not new, and will remain the effect that does nothing.
Effect.task
This endpoint allows you to spin up an async context that will feed an action back into a reducer as some later date.
While
Effect.task
was introduced awhile ago, we’ve made a few changes.Previously, we documented that you should not use
Effect.task
directly in your reducers, as the async context made testing precarious. This is no longer the case, as we have improved our scheduling and testing tools to accommodate async/await (see "Changes to the test store" and "Changes to Combine Schedulers").Effect.task
was also originally overloaded with throwing and non-throwing versions:We have consolidated this into a single endpoint that returns a non-failing
Effect<_, Never>
that uses an optional "catch" block for handling errors:operation
throws aCancellationError
, the effect will be ignored and no action will be fed back into the system.operation
throws anything else,catch
will be invoked to feed an error handling action back into the system.catch
is not provided, a runtime warning / test failure will be generated.For more on error-handling ergonomics and
Effect.task
, see "Task results".The version of
Effect.task
that returnsEffect<_, Error>
has been soft-deprecated, as it requires you to perform additional work with Combine operators to catch errors.Effect.run
This is a new endpoint (not to be confused with the existing, Combine-friendly API). Like
Effect.task
, it allows you to spin up an async context, but instead of returning a single action to be fed back into the reducer, it can feed multiple actions back into the reducer over time via asend
continuation.The
send
parameter, likeViewStore.send
, accepts an optionalanimation
parameter for performing SwiftUI animations:Error handling is done the same way as
Effect.task
: throwing aCancellationError
will terminate the async work, while throwing anything else must be handled in acatch
closure to avoid runtime warnings and test failures.Effect.fireAndForget
This endpoint allows you to spin up an async context that will never feed an action back into the reducer.
While a synchronous version of this endpoint has existed for a long time, the async-friendly version was introduced just recently.
Unlike
Effect.task
andEffect.run
, errors are ignored, and the context will terminate as soon as one is thrown.Effect cancellation
Effect cancellation remains mostly the same, with a few improvements.
First,
ViewStore.send
now returns a task that can be awaited/cancelled directly, this can help eliminate extra work done in the reducer to manage cancellation. (See "Changes to the view store" for more.)Second, there are new async-friendly
Task.cancel(id:)
andwithTaskCancellation(id:)
helpers for introducing more fine-grained cancellation to an effect:Task results
We’ve introduced a new type,
TaskResult
to aid in performing failable work in the Composable Architecture. It can be used to wrap the output of an effect:So why use a
TaskResult
instead of aResult
?Swift error handling is currently untyped, which means an error thrown from an async effect is an
any Error
. TCA’s testing tools require a feature’s state and actions be equatable for assertions to be made, so modeling an effect response withResult
is incompatible with these tools.It has been common to dance around this by replacing errors with custom equatable error types, instead, but this requires a lot of boilerplate.
TaskResult
eliminates that extra work.If you attempt to assert against a task result failure with a non-equatable error, the test will fail. Simply conform the error type you test to
Equatable
to get a passing test.Changes to the view store
ViewStore.send
now returns aViewStoreTask
, which is a handle to the lifecycle of any effects the reducer returns for the action.This task can be awaited or cancelled, which means a SwiftUI view can use a
task
view modifier to automatically cancel and tear down long-living effects spun up by the reducer:This makes it much easier for child features to clean up their long-living effects automatically, and will help avoid runtime warnings commonly encountered when using
optional
andforEach
reducers.Changes to the test store
In order to test a reducer that uses async effects, the test store must be made more async-aware. There are two main changes to be aware of:
Async
TestStore.receive
TestStore.receive
has a new async overload, which will await the action to be received from an effect to give the concurrency runtime enough time to deliver the action.The non-async version of
receive
has been soft-deprecated.TestStoreTask
Where
ViewStore.send
returns aViewStoreTask
that can be cancelled/awaited,TestStore.send
now returns aTestStoreTask
that can do the same. This means if you write a test that spins up a long-living effect for a view’s task modifier, you can test its cancellation:You can also wait for a test store task to finish before making an assertion:
Changes to Combine Schedulers
TCA has heavily relied on our Combine Scheduler library to make time-based features more testable. While Swift 5.7 introduces the
Clock
protocol and paves a way towards writing time-based features without Combine, we will still benefit from improved tooling in the meantime.As such, we’ve introduced the following changes to Combine Schedulers:
Async
TestScheduler.advance
TestScheduler.advance
has a new async overload for async contexts. It automatically suspends its task as it advances and schedules work to be performed.Scheduler.sleep
andScheduler.timer
There are new async endpoints on
Scheduler
for sleeping a task or spinning up a timer. Use these endpoints insideEffect.task
andEffect.run
to schedule work and remain synchronously testable:These methods can be thought of as replacements for the
Effect.delay
andEffect.timer
endpoints.Trying the beta
To give the beta a shot, update your SPM dependencies to point to the
concurrency-beta
branch:This branch also includes updated demo applications using these APIs, so check them out if you're curious!
We hope these new tools make TCA more fun and easier to use than ever. If you take things for a spin, please let us know if you have any questions, comments, concerns, or suggestions!
Beta Was this translation helpful? Give feedback.
All reactions