pointfreeco / swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
https://www.pointfree.co/collections/composable-architecture
MIT License
12.22k stars 1.42k forks source link

Non-main thread warning when calling using .refreshable from child @ViewBuilder property #3067

Closed uniqby closed 4 months ago

uniqby commented 4 months ago

Description

If you add the .refreshable modifier to the child property in @ViewBuilder, attempting to refresh the list triggers a warning that the call was made on a non-main thread:

"Store.send" was called on a non-main thread with: Test.Action.refresh …

The "Store" class is not thread-safe, and so all interactions with an instance of "Store" (including all of its scopes and derived view stores) must be done on the main thread.

received action: Test.Action.refresh (No state changes)

received action: Test.Action.increment Test.State(

  • _count: 0
  • _count: 1 )

Example:

struct TestView: View {
    let store: StoreOf<Test>

    var body: some View {
        NavigationStack {
            list
        }
    }

    @ViewBuilder
    var list: some View {
        List {
            Text(verbatim: "Count: \(store.count)")
        }
        .refreshable {
            await store.send(.refresh).finish()
        }
    }
}

However, if you place the list directly in the body, everything works as expected without warnings:

received action: Test.Action.refresh (No state changes)

received action: Test.Action.increment Test.State(

  • _count: 0
  • _count: 1 )

Example:

struct TestView: View {
    let store: StoreOf<Test>

    var body: some View {
        NavigationStack {
            List {
                Text(verbatim: "Count: \(store.count)")
            }
            .refreshable {
                await store.send(.refresh).finish()
            }
        }
    }
}

Checklist

Expected behavior

No non-main thread warning

Actual behavior

Store.send" was called on a non-main thread with: Test.Action.refresh …

The "Store" class is not thread-safe, and so all interactions with an instance of "Store" (including all of its scopes and derived view stores) must be done on the main thread.

Steps to reproduce

No response

The Composable Architecture version information

1.10.3

Destination operating system

iOS 17.4

Xcode version information

Version 15.3 (15E204a)

Swift Compiler version information

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0
mbrandonw commented 4 months ago

Hi @uniqby, this is just how Swift/SwiftUI behaves, for better or worse. The body of views is marked as @MainActor(unsafe), and so everything inside inherits @MainActor, including refreshable. But as soon as you move some of the view out to another computed property, it loses the @MainActor. You can either mark the list property as @MainActor, or you can use refreshable { @MainActor in … }.

Since this is not an issue with the library I am going to convert it to a discussion. Feel to free to continue the conversation over there.