SwiftUI @MainActor loses global actor
Asked Answered
E

1

12

I've got an ObservableObject class that I am calling to via @StateObject from my view. Inside that class is a function and the entire class is marked @MainActor. In my view there is a TextField with an onChange modifier that has a perform calling to that function via viewModel.funcname.

Xcode is complaining with an error of:

Converting function value of type '@MainActor (String) -> Void' to '(String) -> Void' loses global actor 'MainActor'

I've been researching this for hours now and have found little to nothing. Even Apple's own docs say to simply use await but that doesn't work, at least not the many ways I've tried it.

This is the code within my view:

    TextField("", text: $viewModel.username)
        .onChange(of: viewModel.username, perform: viewModel.editingChanged) // This is where the error occurs

This is the function in my class (remember that the entire class is marked @MainActor):

func editingChanged(_ value: String) {
            
    let jsonFetchUserExistsURL = URL(string: "https://blah.com")
    let jsonFetchUserExistsTask = jsonFetch(jsonFetchUserExistsURL, defaultValue: [UserExists]())

    guard isNetworkActive else { loadingAlert = true; return }
        Task {
            jsonFetchUserExistsTask.sink (receiveCompletion: { completion in
                switch completion {
                    case .failure:
                        self.loadingState = .failed
                    case .finished:
                        self.checkUser()
                }
            },
            receiveValue: { loadedUserExists in
                self.userExists = loadedUserExists
            }).store(in: &requests)
        }
    }
}

I have tried modifying the onChange to read as follows:

.onChange(of: viewModel.username, perform: await viewModel.editingChanged)
.onChange(of: viewModel.username, perform: Task { await viewModel.editingChanged })
.onChange(of: viewModel.username, perform: Task.detached { await viewModel.editingChanged })
.onChange(of: viewModel.username, perform: DispatchQueue.main.async { viewModel.editingChanged })

The entire reason I marked the class @MainActor is because Xcode complained that the function wasn't running on the main thread. It compiled but froze after it complained a few times in the console.

Nothing I've tried seems to change anything. Hoping someone can shed some light on this.

Epilimnion answered 12/2, 2022 at 0:45 Comment(2)
"The entire reason I marked the class @MainActor is because Xcode complained that the function wasn't running on the main thread" If jsonFetchUserExistsTask is an asynchronous operation (which I'm guessing it is) and isn't returning on the main thread, it seems like rather than going through the dance of trying to annotate with @MainActor and then launch a Task directly within editingChanged, which then calls jsonFetch on another thread, you should just use .receive(on: RunLoop.main) before your sink...Intermigration
The jsonFetch uses combine and is already marked to run on the main thread via .receive(on: DispatchQueue.main)Epilimnion
I
8

See my comment on the question about whether or not the @MainActor strategy is the right way to address an underlying issue, but to directly address your compilation error, you can use this syntax, which compiles fine:

.onChange(of: viewModel.username) { viewModel.editingChanged($0) }
Intermigration answered 12/2, 2022 at 1:1 Comment(3)
That worked. Thanks. Now I'm tracking down why it's going into an infinite loop all of a sudden. Separating out the functions to clean up the code is a serious PITA.Epilimnion
sad we have to go through thisSargasso
Can someone explain why this works? The closure is not @MainActor, so shouldn’t the compiler require that calling editingChanged be async?Deliberate

© 2022 - 2024 — McMap. All rights reserved.