TCA with Swift 5.9 If and Switch Expressions: The Compiler Error Workaround

Greetings

Hello, I’m Ong Yue Huei (AJ), an iOS software engineer at Money Forward, Inc.‘s Tax Return Group.

Months ago, Swift 5.9 dropped with some fresh if and switch expressions that excited many, including myself. When I first caught wind of it, akin to many of you, I snagged my PC and dove into some hands-on tinkering. Everything was positive until I hit the compile button.

In this article, I’ll discuss the problem I ran into with the new syntax and the workaround I came up with. I will break it down into three main parts: the problem, what I’ve explored and the workaround.

Before I dive in, just a heads up—I’m working with the Swift Composable Architecture (TCA) version 1.6.0 and Xcode version 15.2.

The Problem

When I tried to compile, the compiler got mad without any additional information—just one line of text in red saying, Command SwiftCompile failed with a nonzero exit code.

What I’ve explored

  1. The new syntax in Reducer’s body without explicitly specifying the type

Compile result: failure

var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .update:
            let text = if .random() { "hoge" } else { "" }
            return .none
        }
    }
}
  1. The new syntax in Reducer’s body and explicitly specifying the type String

Compile result: failure

var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .update:
            let text: String = if .random() { "hoge" } else { "" }
            return .none
        }
    }
}
  1. The new syntax in Reducer’s effect

Compile result: success

var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .update:
            return .run { _ in
                let text = if .random() { "hoge" } else { "" }
            }
        }
    }
}

When I tried to return an effect to perform the work, it successfully compiled! But I’m pretty sure this new syntax isn’t causing any intense work in the reducer.

Since the new syntax can be compiled without any errors in the effect, I continued experimenting with the Reducer’s body.

  1. The new syntax in a closure and called from the Reducer’s body

Compile result: success

var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .update:
            let text = { if .random() { "hoge" } else { "" } }()
            return .none
        }
    }
}

Now things are getting interesting. Swift’s computed property seems suspicious, so I tried out another piece of code to make sure it’s not up to no good.

  1. The new syntax in a computed property (without TCA)

Compile result: success

var text: String { if .random() { "hoge" } else { "" } }

Okay, now that I know that it’s not something with the computed property, I decided to reduce the compiler’s workload by replacing .random() with true.

  1. The new syntax in Reducer’s body, with compiler’s workload reduced

Compile result: failure

var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .update:
            let text =  if true { "hoge" } else { "" }
            return .none
        }
    }
}

Since it failed again, I tried to further reduce the compiler’s workload by explicitly specify the type.

  1. The new syntax in Reducer’s body, with further reducing the compiler’s workload by explicitly specifying the type String

Compile result: success!!

var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .update:
            let text: String =  if true { "hoge" } else { "" }
            return .none
        }
    }
}

After all these trials, it seems like the new syntax is a bit too much for the compiler to handle right now. I assume that the compiler is still getting the hang of it so let’s just give it some time.

PS: I’ve submitted an issue for this.

The Workaround

For now, if you are like me, totally into the new syntax and wanting to use it in your TCA project, here’s a tips for you.

We know there are two reducers, the func reduce(into state: inout State, action: Action) -> Effect<Action> reducer and the var body: Body { get } reducer.

I’ve noticed that many prefer using body: Body instead of func reduce(into:action:) because it has that SwiftUI vibe and it’s easier to compose in the logic from other reducers in the future.

But in order to tackle the compile issue, I would suggest creating a func core(into:action:) that mirrors the structure of the func reduce(into:action:) Reducer and later call it from the body: Body Reducer. Here’s how it looks.

var body: some ReducerOf<Self> {
    Reduce(core)
}

func core(state: inout State, action: Action) -> Effect<Action> {
    switch action {
    case .update:
        let text =  if .random { "hoge" } else { "" }
        return .none
    }
}

The official documentation provides a similar syntax for this.

With this, we can:

  • Apply the new if and switch syntax
  • Preserve the SwiftUI-like structure for the reducer
  • Ensure convenient composition of reducers for the future
  • Prevent the compiler from getting mad!

That’s all. Thanks for reading, happy coding!

[Update]

It turns out that the issue is sorted in the upcoming release (Tested with the latest swift-5.10-DEVELOPMENT-SNAPSHOT-2024-01-23-a snapshot). If you are not really a fan of the workaround, let’s hold back our love for the new syntax and wait for the release.

Published-date