Skip to content

Instantly share code, notes, and snippets.

@tciuro
Last active March 29, 2024 11:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tciuro/ef04030689c29a27e3d2c6dc1bdeaad6 to your computer and use it in GitHub Desktop.
Save tciuro/ef04030689c29a27e3d2c6dc1bdeaad6 to your computer and use it in GitHub Desktop.
SwiftUI Form Highlight Issue
//
// ContentView.swift
// FormRowHighlightIssue
//
// Created by Tito Ciuro on 3/27/24.
//
import SwiftUI
enum Route: Hashable {
case one
case two
case three
}
@Observable
final class SomeViewModel {
var count: Int = 0
@MainActor
func setRandomCount() {
count = Int.random(in: 1 ... 99)
}
}
@MainActor
struct ContentView: View {
@State private var viewModel = SomeViewModel()
@State private var navigationPath: [Route] = []
var body: some View {
NavigationStack(path: $navigationPath) {
Form {
Section {
NavigationLink(value: Route.one) {
Label("\(viewModel.count)", systemImage: "stethoscope")
}
NavigationLink(value: Route.two) {
Label("Two", systemImage: "stethoscope")
}
}
Section {
NavigationLink(value: Route.three) {
Label("\(viewModel.count)", systemImage: "stethoscope")
}
}
}
.navigationDestination(for: Route.self) { route in
switch route {
case .one:
Text("One")
case .two:
Text("Two")
case .three:
Text("Three")
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Selection Issue")
.task {
viewModel.setRandomCount()
}
}
}
}
#Preview {
ContentView()
}
@mattmassicotte
Copy link

Ok, so I really want to stress here that I know very little about SwiftUI. But my gut is that this isn't a concurrency problem but actually just ordering of operations. You mentioned that this also happens with onAppear, and that makes me think this is even more likely to not be concurrency-related.

However, I do still have a comment about how you are using task. You have added @MainActor on the closure, and that's kind of a red flag to me. body is always @MainActor and task will inherit the current actor context. So, the annotation is redundant. That's fine, except I always get worried about redundant annotations, not for style, but in case they are exposing a gap in understanding.

I also think you may ultimately need to make SomeViewModel @MainActor. But I don't think that is a factor in this specific problem.

@tciuro
Copy link
Author

tciuro commented Mar 28, 2024

Got it. If I annotate SomeViewModel with @MainActor, I get Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context in line 26.

So I tried this:

    @MainActor
    func setRandomCount() {
        count = Int.random(in: 1 ... 99)
    }

but I'm still seeing the issue.

@mattmassicotte
Copy link

setRandomCount, being a function on a MainActor type is also itself MainActor.

That warning is basically saying on line 26 you aren't on the MainActor. And that's just bizarre, but part of how SwiftUI is designed, for better or worse. A solution here is to also mark this View as MainActor. In my opinion, all SwiftUI views should always be MainActor isolated. Because anything else is just terribly confusing. Though there actually are other potential non-MainActor use-cases.

@tciuro
Copy link
Author

tciuro commented Mar 28, 2024

This feels like playing whack-a-mole. What a mess. That all SwiftUI views should always be MainActor isolated seems reasonable to me. I wonder why Apple doesn't make that the default, and if anything, have an option to opt-out. All these @MainActor everywhere feels truly awful. Like... we have no effing clue what we're doing (I'm first in line.)

@mattmassicotte
Copy link

I wrote about this a little: https://www.massicotte.org/swiftui-isolation

There are things you can do to make this more automatic, including the somewhat extreme option I have here: https://github.com/mattmassicotte/ConcurrencyRecipes/blob/main/Recipes/SwiftUI.md

But I'm afraid I don't think this is related to your original issue....

@tciuro
Copy link
Author

tciuro commented Mar 29, 2024

Did read thoroughly, thanks for the info. Honestly, I hesitate to touch anything without knowing what's going on. I really feel like this is a SwiftUI bug. One thing I've done is add a view state manager I wrote a while ago, similar to Michael Long's solution. It works fine because when the state changes, it redraws the view. But I feel it's a heavy hammer I shouldn't have to be using.

@mattmassicotte
Copy link

I’m just not sure. And it may be a bug! I just don’t have enough SwiftUI experience to help I’m afraid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment