How does Swift 6 treat Objective-C types for Sendable and actor isolation, and what annotations help?
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_ACTORto 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…