skip to Main Content

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?

2

Answers


  1. 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 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.
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search