How do you work with Non-sendable types in swift?
Asked Answered
E

1

13

I'm trying to understand how to use an Apple class, without Sendable, in an async context without getting warnings that this won't work in Swift 6.

Weapon of choice is NSExtensionContext which I need to fetch a URL that's been passed into a share extension.

This is my original code that simply fetches a URL from the extension context. With the new concurrency checking enabled it gives the warning:

Main actor-isolated property 'url' can not be mutated from a Sendable closure; this is an error in Swift 6

class ShareViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        fetchURL()
    }

    private func fetchURL() {
        
        guard let extensionContext = self.extensionContext,
              let item = extensionContext.inputItems.first as? NSExtensionItem,
              let attachments = item.attachments else { return }
        
        for attachment in attachments {
            
            if attachment.hasItemConformingToTypeIdentifier("public.url") {
                attachment.loadItem(forTypeIdentifier: "public.url") { url, error in
                    
                    guard let url else { return }
                    
                    self.url = url as? URL <-- WARNING! Main actor-isolated property 'url' can not be mutated from a Sendable closure; this is an error in Swift 6
                }
            }
        }
    }
}

I understand the function is called on the MainActor but the extensionContext can be used on any actor which is the reason for the complaint.

Firstly can I perhaps mark the url property as Sendable so it can be modified from any actor?

Trying something different, I modified it to use the latest async/await versions of extensionContect.

override func viewDidLoad() {
    super.viewDidLoad()
    
    Task {
        await fetchURL()
    }
}

private func fetchURL() async {
    
    guard let extensionContext = self.extensionContext,
          let item = extensionContext.inputItems.first as? NSExtensionItem,
          let attachments = item.attachments else { return }
    
    for attachment in attachments {
        
        if let url = try? await attachment.loadItem(forTypeIdentifier: "public.url") as? URL { <-- WARNINGS!
            self.url = url
        }
    }
}

This actually gives me 4 warnings on the same line!

Non-sendable type 'any NSSecureCoding' returned by implicitly asynchronous call to nonisolated function cannot cross actor boundary

Passing argument of non-sendable type '[AnyHashable : Any]?' outside of main actor-isolated context may introduce data races

Passing argument of non-sendable type '[AnyHashable : Any]?' outside of main actor-isolated context may introduce data races

Passing argument of non-sendable type 'NSItemProvider' outside of main actor-isolated context may introduce data races

Let's try detaching the Task so it runs on a new actor:

private func fetchURL() async {
    
    guard let extensionContext = self.extensionContext,
          let item = extensionContext.inputItems.first as? NSExtensionItem,
          let attachments = item.attachments else { return }
    
    Task.detached {
        for attachment in attachments { <-- WARNING! Capture of 'attachments' with non-sendable type '[NSItemProvider]' in a `@Sendable` closure
            let attachment = attachment
            if let url = try? await attachment.loadItem(forTypeIdentifier: "public.url") as? URL {
                await MainActor.run { [weak self] in
                    self?.url = url
                }
            }
        }
    }
}

Just the 1 warning with this:

Capture of 'attachments' with non-sendable type '[NSItemProvider]' in a @Sendable closure

Final try, let's put everything in the detached actor. This requires accessing the extensionContext asynchronously using await:

private func fetchURL() async {
    
    Task.detached { [weak self] in
        guard let extensionContext = await self?.extensionContext, <-- WARNING! Non-sendable type 'NSExtensionContext?' in implicitly asynchronous access to main actor-isolated property 'extensionContext' cannot cross actor boundary
              let item = extensionContext.inputItems.first as? NSExtensionItem,
              let attachments = item.attachments else { return }
        
        for attachment in attachments {
            let attachment = attachment
            if let url = try? await attachment.loadItem(forTypeIdentifier: "public.url") as? URL {
                await MainActor.run { [weak self] in
                    self?.url = url
                }
            }
        }
    }
}

We get the error:

Non-sendable type 'NSExtensionContext?' in implicitly asynchronous access to main actor-isolated property 'extensionContext' cannot cross actor boundary

I know 1 way to clear all the warnings:

extension NSExtensionContext: @unchecked Sendable {}

The problem I have with this, is using @unchecked seems to be like telling the compiler to just ignore the consequences.

What would be the correct way to use this extensionContext in a UIViewController that runs on @MainActor?

Elviaelvie answered 12/3 at 15:7 Comment(1)
Double check but I think all the properties for NSExtensionContext are get only the extension should be ok because there isn't any mutating.Trigonous
R
13

Your second example using the async version of loadItem is what you're going to want eventually. SE-0414 (Region based isolation) should fix the warning when it is shipped.

In the meantime, your first example is simply incorrect. It's a race condition, since loadItem does not promise to call its closure on the main actor:

The block may be executed on a background thread.

Swift is correctly warning you about this bug. You can fix it as usual, by moving the actor's update to the actor's context:

Task { @MainActor in
    self.url = url as? URL
}

This will leave you with the NSSecureCoding warning. That one is because Foundation and UIKit are not yet fully annotated. Until they are, you should import as @preconcurrency when you need to. (The compiler will warn you if you add the annotation unnecessarily.)

@preconcurrency import UIKit

With these two changes, I see no warnings in Xcode 15.3 under "complete" concurrency.

Rubinstein answered 12/3 at 17:24 Comment(5)
Thanks Rob, great answer. I didn't know about @preconcurrency. It also seems Swift is still not 100% ready for this switch. I thought this 5.10 release was my last chance to get everything right.Elviaelvie
Luckily, there's no "last chance." The Swift 6 model is opt-in, on a per-package basis, and likely will be for several years. But 5.10 is the first release that has all known data race holes plugged, so I think it is a really good time to start the migration to concurrency=complete. I'm in the process of doing that on a couple of million lines of code right now. You should expect to see @preconcurreny markings for several years, much as we saw implicitly unwrapped optionals persist for several years. Annotating Optionals in Foundation took a long time, but we made it through.Rubinstein
I am really looking forward to SE-0414. It addresses a glaring problem of Swift concurrency (and saves them from having to go on a holy crusade of adding Sendable conformance for tons of types that don't really need it). But, as always, great answer (+1).Transmigrant
As an aside, have you had luck testing this? I tried Xcode 15.3 with -enable-experimental-feature RegionBasedIsolation under “Other Swift Flags”, and it does not appear to be working. I've also tried the latest toolchain PR-72287-1165, also with no success. Have you had luck testing this, and if so, with which toolchain/configuration?Transmigrant
I thought RegionBasedIsolation as only on main; I don't know if it's in a release yet. I haven't tried any toolchains but Xcode.Rubinstein

© 2022 - 2024 — McMap. All rights reserved.