Why is a static let CGFloat in a View an async property in swift 6?

struct TrimView: View {

    static let thumbnailHeight: CGFloat = 70
    static let thumbnailSidePadding: CGFloat = 100

My property is simple. Defined in my struct.
But when I want to access it elsewhere...

Presumably - TrimView is inheriting @MainActor from the View protocol.

But:

  1. I can't see anywhere in the docs that say View inherits @MainActor
  2. It's a static class constant. It's immutable by definition. Surely this is safe???

What's going on here?

1 Like

View protocol is not isolated on main actor, only its body property, unless you are using custom View protocol marked with @MainActor which shadows SwiftUI.View version. In that case your static property is isolated on main actor, and if you try accessing it from different isolation, it will require to be marked with await since access to it is isolated to main actor.

This constants could be safe to access without isolation if you detach them from the view:

struct TrimUIConstants {
    static let thumbnailHeight: CGFloat = 70
    static let thumbnailSidePadding: CGFloat = 100
}

I kinda figured it out.
This is triggered by having an @ObservableObject in the view

It's absurd and broken to me that adding a variable transforms the entire struct to @MainActor - but it seems so.

I'm still not clear why accessing a class constant isn't a safe operation though.
The value is literally inline-able, it shouldn't be a concurrency failure...

Minimal reproduction case

struct ContentView: View {
    @ObservedObject var vm =  Foo()
    
    static let const:CGFloat = 100
    
    var body: some View {
        Text("Hello")
    }
}

class Foo:ObservableObject {
    func bar() {
        let task = Task {
            print("\(ContentView.const)")
        }
    }
}

1 Like

As you suggest, it seems the properties are getting isolated to an Actor, most likely the @MainActor. Based on the code sample you posted, I don't quite understand why.

Another likely workaround would be to use the nonisolated keyword on the properties.

struct TrimView: View {
    nonisolated static let thumbnailHeight: CGFloat = 70
    nonisolated static let thumbnailSidePadding: CGFloat = 100
}

Which version of Swift are you using? That behaviour should be gone in Swift 5.9: swift-evolution/proposals/0401-remove-property-wrapper-isolation.md at main · apple/swift-evolution · GitHub

1 Like

There are too few information you've provided to figure out what in your case causes this behaviour. But more likely using @StateObject / @ObservedObject in a view causes actor isolation to be extended to the whole view, I thought that inference has been removed by default with proposal. (wrong take here)

My bad - its hidden behind the feature flag: -enable-upcoming-feature DisableOutwardActorInference
cc @ConfusedVorlon

1 Like

No problem with workarounds - I can sort that.

Just interested in the wierd behaviour.

XCode 15.3

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)

Re actor isolation, if I take out the @ObservedObject, the problem goes away - so I'm pretty certain the @ObservedObject is causing the isolation issue.

Also interesting - the error only happens within a Task context.


class Foo:ObservableObject {
    func bar() {
        //This is fine
         print("\(ContentView.const)")

        let task = Task {
            //This generates a warning
            print("\(ContentView.const)")
        }
    }
}

because - sure - swift doesn't need to make sense!

I provided the entire code and xcode version :rofl:

Oh, sorry, I've confused who is topic starter, sleepless night :slight_smile:

1 Like

That was/is a strange behaviour indeed. One note on overall view isolation - I recommend isolate SwiftUI views on main actor entirely by default:

@MainActor
struct MyView: View {
    // ...
}

I still don't understand what was the driver for Apple engineers not to make View protocol isolated initially, but providing such isolation on your views helps to get more consistent behaviour and much less surprises.

Yeah, this still puzzles me. I'm generally against requiring whole-type isolation on protocols, because it's usually the wrong thing to do practically and semantically, but in this case the only types that can possibly conform to View are structs, so (IMO) it is actually reasonable to require @MainActor.

Although, you'd still run into [what I believe is] a design flaw whereby you don't need to [re]state the @MainActor on your conforming type, making it too hidden that the type has that restriction. So in effect you should be writing @MainActor on your views irrespective of whether View requires it.

3 Likes

I hate all the implicit isolation regimes.
There is just way too much going on that is hidden from view!

I much prefer explicit annotations.