DEV Community

Oleksii Ratiiev
Oleksii Ratiiev

Posted on

How to use .refreshable with TCA in Swift

What's the Issue?

To implement pull-to-refresh in SwiftUI, you need to use the .refreshable modifier, but here is the problem:

  • If you simply add the .refreshable modifier to your Scroll or List view and send an action to perform the refresh request, you'll notice that the loader disappears immediately.
.refreshable {
     send(.refresh)
}
Enter fullscreen mode Exit fullscreen mode

Loader disappears immediately GIF

This happens because we don't wait for the completion of the request. To address this, we must call .finish() on our send call and await it:

.refreshable {
     await send(.refresh).finish()
}
Enter fullscreen mode Exit fullscreen mode

Loader keep showing during request GIF
Adding a Refresh Completion Notification

Another useful thing is to get notified when the refresh has finished. To achieve this, let's add a state variable that will hold true while the refresh is in progress. We'll also add an action, .refreshFinished, which will be sent when the refresh scope is completed. We'll then update our state accordingly.
State

    @ObservableState
    struct State: Hashable, Sendable {
        //...
        var isRefreshing: Bool = false
    }

    public enum Action : Equatable, Sendable, ViewAction {
        case view(ViewAction)

        public enum ViewAction: Equatable, Sendable {
            case refreshFinished
            case refresh
        }
    }

//later in reducer
        case .refreshFinished:
            state.isRefreshing = false
            return .none
        case .refresh:
            state.isRefreshing = true
            return .run { _ in
                try await clock.sleep(for: .seconds(2))
            }
Enter fullscreen mode Exit fullscreen mode

To send refreshFinished, we'll utilize the defer keyword, which is basically the same as try/finally block in other languages.
View

    var body: some View {
            List {
                ForEach(store.items, id: \.self) { item in
                    Text(item)
                }
            }
            .refreshable {
                defer { send(.refreshFinished) }
                await send(.refresh).finish()
            }
    }
Enter fullscreen mode Exit fullscreen mode

Implementing Refresh Cancellation

Let's add another feature to allow users to cancel the refresh. Since we now have a state variable, we can simply add a cancel button whenever the refresh is in progress and send a .cancelRefreshTapped action.

                if store.isRefreshing {
                    withAnimation(.easeIn) {
                        Button("Cancel") {
                            send(.cancelRefreshTapped)
                        }
                    }
                }
Enter fullscreen mode Exit fullscreen mode

We'll also need to create a CancelID to identify which request to cancel

private enum CancelID { case refreshRequest }
Enter fullscreen mode Exit fullscreen mode

and mark the refresh effect as .cancellable, providing the appropriate CancelID.

        case .refresh:
            state.isRefreshing = true
            return .run { _ in
                try await clock.sleep(for: .seconds(2))
            }
            .cancellable(id: CancelID.refreshRequest)
Enter fullscreen mode Exit fullscreen mode

Finally, we need to cancel the request when the .cancelRefreshTapped action is received.

        case .cancelRefreshTapped:
            return .cancel(id: CancelID.refreshRequest)
Enter fullscreen mode Exit fullscreen mode

Conclusion

That's it! We now have a fully functional pull-to-refresh implementation with proper handling of loading states, completion notifications, and cancellation.
Full Code can be found here
Similar example from TCA Case Studies repo

Feedback Welcome

If you enjoyed this post, please leave a reaction. If you have any alternative approaches to implementing this, I would appreciate your comments.

About This Blog

I started this blog to share the things I learn and believe can be helpful to others, so follow for more. Thanks (˶ᵔ ᵕ ᵔ˶)

Top comments (0)