Accessing MainActor isolated properties, from nonisolated View function (PhotosPicker)

I cannot find a clean way to resolve 2 issues related to the fact that SwiftUI's View.overlay() is declared as nonisolated and i need the content to access MainActor properties from the view model:

EDIT: please find the code with actual reproducible issue and updated description in a further answer below: Accessing MainActor isolated properties, from nonisolated View function (PhotosPicker) - #5 by jalone

@MainActor class FooVM: NSObject, ObservableObject {

    enum Bar {
        case bar
    }

    @Published  var bar: Bar = .bar
}

struct FooView: View {

    @StateObject var vm: FooVM

    var body: some View { foo }

    @ViewBuilder var foo: some View {
        AnotherView()
            .overlay {
                    switch vm.bar { // 1. WARNING
                    case .bar:
                        ProgressView()
                            .progressViewStyle(.circular) // 2. WARNING
                    }
            }
    }

}

1. WARNING

The first warning is Main actor-isolated property 'bar' can not be referenced from a nonisolated context; this is an error in the Swift 6 language mode

where with nonisolated context it refers to the declaration of .overlay():
@inlinable nonisolated public func overlay<V>(alignment: Alignment = .center, @ViewBuilder content: () -> V) -> some View where V : View

2. WARNING

An similar error but of which i have even less freedom to solve is that progressViewStye is also nonisolated whille .circular is declared MainActor

@MainActor @preconcurrency public static var circular: CircularProgressViewStyle { get }

WHAT I TRIED

Beside finding it a very ugly solution, I cannot wrap the switch case in a MainActor.assumeIsolated since i get errors when i have to return the view from the various cases, for example
Branches have mismatching types 'some View' (result of 'Self.foregroundColor') and 'some View' (result of 'Self.foregroundColor') and the Views hierarchy is complex.

Also these are just a few of the crazy amount of warning that have popped since i updated to xcode 16, despite having a perfectly clean build with strict concurrency complete enabled in xcode 15.5

Your code compiles for me in Xcode 16.0 in Swift 6 language mode without any @preconcurrency import, so long I add var body to the view. Also, virtually every view modifier is nonisolated, so this does not relate to overlay specifically.

It could be that the diagnostics are failing you in this way specifically because of the lack of var body: the View protocol is @MainActor, and the modifiers, being synchronous, will inherit that, but without the body property the compiler can't confirm that the struct is indeed a View and thus doesn't designate it to the main actor. When I remove the View conformance, that is, write simply

struct FooView {
     // ...
}

I'm getting the errors you've mentioned.

Now, it's unfortunate that the diagnostics for concurrency are firing before the diagnostics for protocol conformance, as it's obviously getting super cryptic and points into a wrong direction — but this doesn't happen for me in Xcode 16.0 16A242d. What is your exact version?

4 Likes

I'm sorry, the var body is present, it's a big project, i tried to strip down as much as possible for clarity here. I add it to the question.

The project is published, and it did compile and with no warning in xcode15, swift5.9 with strict concurrency enabled; and then i get all this warnings with xcode16 (which become errors if i enable swift6).

It sometimes happens to me that the compiler reports irrelevant errors on SwiftUI views because the expressions are very complex, and it apparently just decides to not diagnose all of them.

In any case, your code looks alright and should work/compile, there's no need to do anything with concurrency specifically, but as I mentioned, it's likely that the compiler doesn't understand that this is a View to begin with, presumably because there's a bug in body. When this happens, I typically would start replacing complex expressions with EmptyView() and comment out large stacks of modifiers until I see which of them fails. You also can put @MainActor over FooView to reassure the compiler and make it typecheck deeper.

1 Like

Thanks for the feedback, you were right the error was misleading, i have found the actual culprit of the problem:

It's the PhotosPicker, updating the same example, this is reproducible.

struct FooView: View {
    
    @MainActor @State var bar: Bool = false
    
    var body: some View {
        
        PhotosPicker(selection: .constant(nil),
                     photoLibrary: .shared()) // PICKER CLOSURE
        {
            if bar { // 1. WARNING
                ProgressView()
                    .progressViewStyle(.circular) // 2. WARNING
            }
        }
    }
}

The warning has also changed to

Main actor-isolated property 'bar' can not be referenced from a Sendable closure

The init of PhotoLibrary, in the standard PhotosUI module has the following initializer:

@MainActor @preconcurrency public struct PhotosPicker<Label> : View where Label : View {
    @preconcurrency nonisolated public init(selection: Binding<PhotosPickerItem?>,
                                            matching filter: PHPickerFilter? = nil,
                                            preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
                                            photoLibrary: PHPhotoLibrary, @ViewBuilder
                                            label: @Sendable () -> Label) // THE TRIGGERING CLOSURE
}

I still don't know how to fix it tho.

It looks more like an issue in PhotosUI framework, I think it worth filing a radar issue.

1 Like

I will do, would be nice tho to find a workaround in the while

1 Like

You can try to move out from the closure (note that this will require that all passed parties conform to Sendable, which seems to be not a problem, at least with the snippet). It is a tricker for progress style, I have only managed to fix this with nonisolated(unsafe):

@ViewBuilder var foo: some View {
    let bar = vm.bar
    nonisolated(unsafe) let pvs: CircularProgressViewStyle = .circular
    PhotosPicker(...)
}

I think this in fact should be safe, because I have no idea how the styling passing can be unsafe.

1 Like

You should be able to capture bar into closure isolation.

PhotosPicker(selection: .constant(nil), photoLibrary: .shared()) { [bar] in // capture here
    if bar {
        ProgressView()
            .progressViewStyle(.circular)
    }
}
1 Like

sadly does not work,

Capture of 'pvs' with non-sendable type 'CircularProgressViewStyle' in a @Sendable closure

It should work – it specifically disables checks. Check if everything is in place:

import PhotosUI

@MainActor
class FooVM: NSObject, ObservableObject {

    enum Bar {
        case bar
    }

    @Published  var bar: Bar = .bar
}

struct ViewTest: View {

    @StateObject
    private var vm: FooVM

    var body: some View { EmptyView() }

    @ViewBuilder
    var foo: some View {
        let bar = vm.bar
        nonisolated(unsafe) let pvs: CircularProgressViewStyle = .circular
        PhotosPicker(selection: .constant(nil),
                     matching: .images,
                     photoLibrary: .shared()) {
            EmptyView()
                .overlay(alignment: .center) {
                    if case .bar = bar {
                        EmptyView()
                        ProgressView()
                            .progressViewStyle(pvs)
                    }
                }
        }
    }
}

1 Like

That does it! I dint't think about it, thanks

i think it moves the problem, note that the warning has changed.

Went from

Main actor-isolated static property 'circular' can not be referenced from a Sendable closure

to

Capture of 'pvs' with non-sendable type 'CircularProgressViewStyle' in a @Sendable closure

struct FooView: View {
    
    @MainActor @State var bar: Bool = false
    
    var body: some View {
        
        nonisolated(unsafe) let pvs: CircularProgressViewStyle = .circular
             
        PhotosPicker(selection: .constant(nil),
                     photoLibrary: .shared()) // PICKER CLOSURE
        { [bar, pvs] in
            if bar { // 1. WARNING SOLVED
                ProgressView()
                    .progressViewStyle(pvs) // 2. WARNING
            }
        }
    }
}

You don't need to capture pvs in the closure, it re-enables checks by introducing internal-scoped variable.

1 Like

Damn, thanks a lot it's solved, this was eye opening.

Radar here: rdar://FB15441102: PhotosPicker init's parameter Sendable closure does not accept accessing MainActor isolated objects