Tometoyou
Tometoyou

Reputation: 8376

Non-sendable type '[String : Any]?' exiting main actor-isolated context in call to non-isolated instance method XXX cannot cross actor boundary

I have a function that looks like this:

public final class MyClass {
  static let shared = MyClass()

  public func doSomething(dictionary: [String: Any]? = nil) async -> UIViewController {
    return await UIViewController()
  }
}

I call it like this inside a UIViewController:

final class MyViewController: UIViewController {
  @IBAction private func tapButton() {
    Task {
      let vc = await MyClass.shared.doSomething()
    }
  }
}

But it gives me a warning:

Non-sendable type '[String : Any]?' exiting main actor-isolated context in call to non-isolated instance method 'doSomething(dictionary:)' cannot cross actor boundary

Note that you must have Strict Concurrency Checking in your Build Settings set to Complete or Targeted for this to show up in Xcode.

I know that functions inside a UIViewController are isolated to the MainActor and I want tapButton() to be. But what can I do about this to avoid this warning?

Upvotes: 10

Views: 5026

Answers (2)

Sweeper
Sweeper

Reputation: 271420

The dictionary is said to "exit main actor-isolated context" and "cross actor boundary" because MyViewController is a @MainActor class, but MyClass isn't. Whatever will be running doSomething is not the main actor, but the cooperative thread pool.

It is not safe for a non-Sendable type like [String: Any]? to cross actor boundaries. [String: Any]? is not Sendable because Any is not Sendable. Yes, you are only passing nil here, which should be safe, but Swift looks at the types only.

There are a few ways to solve this. Here are some that I thought of:

  • take a [String: any Sendable]? instead
  • annotate MyClass or doSomething with @MainActor
  • use Task.detached instead of Task.init, so that the cooperative thread pool runs the task too. Task.init will inherit the current actor context, and at tapButton, it is the main actor.

Upvotes: 11

Rob Napier
Rob Napier

Reputation: 299345

You can't pass [String: Any] across concurrency boundaries under "targeted" concurrency warnings, since it's not (and can't be) Sendable. Even though it's a default parameter, it's still being created in the calling context and then passed to the receiving context. To avoid that, you need to create the default parameter in the receiving context. Default parameters are just shortcuts for explicit overloads, so you can write the overload explicitly instead:

// Remove the default parameter here
public func doSomething(dictionary: [String: Any]?) async -> UIViewController {
    return await UIViewController()
}

// And define it explicitly as an overload
public func doSomething() async -> UIViewController {
    return await doSomething(dictionary: nil)
}

With this, the call to doSomething() doesn't create a Sendable problem.

That said, I expect that MyClass should actually be @MainActor (or at least doSomething). Creating a UIViewController on a non-main queue is legal if I remember correctly (though it might not be), but no method is legal to call on it outside the main queue.

Upvotes: -1

Related Questions