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

513 Views Asked by At

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?

1

There are 1 best solutions below

5
On BEST ANSWER

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.