Tuesday Nov 14, 2023
To celebrate the release of Swift macros we releasing updates to 4 of our popular libraries to greatly simplify and enhance their abilities: CasePaths, ComposableArchitecture, SwiftUINavigation, and Dependencies. Each day this week we will detail how macros have allowed us to massively simplify one of these libraries, and increase their powers.
Today we are releasing version 1.4 of our popular library, the Composable
Architecture. It introduces a new @Reducer
macro that can automate some of the aspects
of building features in the library, and greatly simplify the tools of the library. Join us for a
quick overview, and be sure to check out the 1.4 migration guide for more detailed
information about how to update your applications.
The new @Reducer
macro can now be used instead of directly conforming to
the Reducer
protocol:
-struct Feature: Reducer {
+@Reducer
+struct Feature {
// ...
}
It’s a very tiny change, but it comes with a number of benefits:
The @Reducer
macro automatically adds the
@CasePathable
macro we announced yesterday to your
feature’s Action
enum, which immediately gives you key path-like syntax for referring to the cases
of your enum. This means you can invoke the various reducer operators that require case paths for
isolating a child feature’s action with a simple key path:
Reduce { state, action in
// ...
}
-.ifLet(\.child, action: /Action.child)
+.ifLet(\.child, action: \.child)
Every API in the library that takes a case path has been updated to be usable with this new syntax.
The @Reducer
macro will also apply the @CasePathable
macro to your feature’s State
type if it is an enum, and further apply the @dynamicMemberLookup
annotation. This allows you to greatly simplify how you use the library’s navigation view modifiers
when dealing with an enum of destinations.
For example, previously the following was necessary to describing driving a sheet from a particular case of an enum of destinations:
.sheet(
store: self.store.scope(state: \.$destination, action: { .destination($0) }),
state: /Feature.Destination.State.editForm,
action: Feature.Destination.Action.editForm
)
It’s quite verbose and unfortunately we cannot leverage type inference to omit the long type names.
But now that getters are derived for each case of the destination enum, we can simplify to just this:
.sheet(
store: self.store.scope(state: \.$destination, action: { .destination($0) }),
state: \.editForm,
action: { .editForm($0) }
)
And in the future the @Reducer
macro may acquire even more powers for
helping you avoid the boilerplate of implementing Destination
features for
tree-based navigation and Path
features for stack-based navigation.
One of the super powers of the Composable Architecture is its ease of testing. However, there is one aspect of testing that is quite verbose, and that is asserting when an effect emits an action.
Currently when you assert that the store receives an action, you have to construct the exact, concrete action:
store.receive(.response(.success("Hello"))) {
$0.message = "Hello"
}
If the store received a different action than the one specified it will fail the test suite. This is very useful for proving you know exactly how your feature is behaving,
This does have a few drawbacks though. First of all, when testing deeply nested features, which is especially common with integration tests, you will need to construct a very verbose, deeply nested enum value:
store.receive(.destination(.presented(.child(.response.success("Hello"))))) {
$0.message = "Hello"
}
Second, the receive
method on TestStore
does an equality check on the action received to make
sure you are exhaustively proving that you know which action is being sent into the system. However,
typically we don’t need to assert on the data inside the action because we already get a decent
amount of coverage on that in the trailing state assertion closure. It also forces the Action
enum
in reducers to be Equatable
, which can be annoying sometimes.
Well, now thanks to the @Reducer
and @CasePathable
macros we have a very short syntax for describing which enum case we expect the store to receive
without specifying the data:
-store.receive(.response(.success("Hello"))) {
+store.receive(\.response.success) {
$0.message = "Hello"
}
And it works especially well when testing deeply nested features too:
-store.receive(.destination(.presented(.child(.response.success("Hello"))))) {
+store.receive(\.destination.child.response.success) {
$0.message = "Hello"
}
And this works even if none of your actions are Equatable
. In fact, because of the simplicity of
this we have even decided to soft-deprecate a type included in the library,
TaskResult
, which only exists to help make actions equatable. Refer to the
1.4 migration guide for more information.
The macro is capable of detecting potential problems in your reducer and alerting you
at compile time rather than runtime. For example, implementing your reducer by accidentally
specifying the reduce(into:action:)
method and the body
property like so:
@Reducer
struct Feature {
struct State {
}
enum Action {
}
func reduce(into state: inout State, action: Action) -> EffectOf<Self> {
…
}
var body: some ReducerOf<Self> {
…
}
}
…is considered programmer error. This is an invalid reducer because the body
property will never
be called. The @Reducer
macro can diagnose the problem, and provide you with
a helpful error message:
@Reducer
struct Feature {
struct State {
}
enum Action {
}
func reduce(into state: inout State, action: Action) -> EffectOf<Self> {
// ┬─────
// ╰─ 🛑 A 'reduce' method should not be defined in a reducer with a
// 'body'; it takes precedence and 'body' will never be invoked.
…
}
var body: some ReducerOf<Self> {
…
}
}
Update your dependency on the Composable Architecture to 1.4 today to start taking
advantage of the new @Reducer
macro, and more. Tomorrow we will discuss how
these new case path tools have massively improved our SwiftUINavigation library.
👋 Hey there! If you got this far, then you must have enjoyed this post. You may want to also check out Point-Free, a video series covering advanced programming topics in Swift. Consider subscribing today!