How does Swift 6 treat Objective-C types for Sendable and actor isolation, and what annotations help?

4.12.2 · L5 · Interview

Predict first

Which of these functions compile in Swift 6 mode? (Plain is an unannotated Objective-C class, Token is NS_SWIFT_SENDABLE.)

// Objective-C: @interface Plain  @property NSInteger n;     NS_SWIFT_SENDABLE @interface Token  @property (readonly) NSInteger value;
func a(_ p: Plain) { let f: @Sendable () -> Void = { print(p.n) }; f() }
  • Compiles
  • Error: capture of non-Sendable type Plain in a @Sendable closure

Predict first

Same shape with the annotated Token?

func b(_ t: Token) { let f: @Sendable () -> Void = { print(t.value) }; f() }
  • Compiles
  • Error: not Sendable

Predict first

An NS_SWIFT_UI_ACTOR class is used from a nonisolated async function. Does s.render() compile?

func c(_ s: Screen) async { s.render() }
  • Yes
  • No: main actor-isolated method cannot be called from outside the actor

Mental model. Without annotations the checker treats every Objective-C class as a shared tool of unknown safety. NS_SWIFT_SENDABLE is a certificate that says “inspected, safe to pass between threads”.

Swift’s concurrency checker cannot read an Objective-C implementation, so it works from the header. An ordinary Objective-C class is not Sendable, and capturing one in a @Sendable closure is an error in Swift 6 mode (a warning in Swift 5 with complete checking). Immutable value-like classes such as NSString, NSNumber and NSDate are known to be safe, as are types you annotate NS_SWIFT_SENDABLE (and that really are immutable or internally synchronised). NS_SWIFT_UI_ACTOR (spelled @MainActor in Swift) marks a class or method as main-actor isolated, so calling it from a background context is an error until you hop with await. NS_SWIFT_NONISOLATED opts a member back out. Handler parameters can be annotated NS_SWIFT_SENDING or @Sendable to describe how the callee uses them.

The annotations are promises, so they should only be added after reading the implementation. For code you cannot change, @preconcurrency import Module downgrades diagnostics about that module’s types to warnings during migration, and @unchecked Sendable or nonisolated(unsafe) on your side are escape hatches that make the responsibility yours. Annotate headers module by module, UI-facing classes first, and let the compiler show where the Swift callers now need await or @MainActor.

An unannotated Objective-C class is not Sendable, so the closure cannot capture it.

NS_SWIFT_SENDABLE imports the class as Sendable.

The annotation makes the class main-actor isolated, so the call needs await from a hop to the main actor.

Common traps

  • Marking a mutable class NS_SWIFT_SENDABLE. The data race is now hidden from every Swift caller.
  • Adding NS_SWIFT_UI_ACTOR to a header and being surprised by errors. Callers on background tasks now need a hop to the main actor, which is the intended result.

They might follow up with

  • How does this relate to Swift-only types? The same rules apply (Sendable, @MainActor); the difference is that the header is the only source of truth.
  • Where do you start in a large app? The leaf modules and the model types, then UI-facing classes.

Related questions

  • 1.10.2 What does Sendable mean for a Swift type you write?
  • 10.10.1 When is a type Sendable, and what does the compiler check when you declare it?
  • 10.10.3 What are @unchecked Sendable and nonisolated(unsafe) promising, and what does the compiler still check when…
  • 10.5.1 How does region-based isolation work, and what does the sending keyword add on top of Sendable?

← Which Swift declarations are exposed to Objective-C without writing…