Which Swift declarations are exposed to Objective-C without writing @objc, and how do you check?

4.12.1 · L3 · Interview

Predict first

Which of these appear in the generated Objective-C header?

import Foundation
@objc protocol Feed { func refresh() }
final class A: NSObject, Feed {
    func refresh() {}
    func plain() {}
    @objc func explicit() {}
    @objc dynamic var viaDynamic = 1
    var notExposed = 2
    func generic<T>(_ t: T) {}
}
class B: NSObject {
    override var description: String { "B" }
    func result() -> Int { 1 }
}
@objcMembers class C: NSObject { func inferred() {}; var count = 0 }
enum Plain { case a }
  • everything except the generic
  • refresh, explicit, viaDynamic, description, inferred and count only
  • only the @objc ones

Predict first

Does this compile?

final class A: NSObject {
    @objc func tup(_ x: (Int, Int)) {}
}
  • Yes
  • No: the parameter type cannot be represented in Objective-C

Mental model. Nothing leaves the Swift building unless it has a visitor badge. A badge can be issued by hand (@objc), by the department (@objcMembers), or by a rule such as “anything that matches the protocol”.

Swift 4 removed most implicit exposure; today a member is exposed to Objective-C only when something explicitly requires it. Rules that apply: a member of an NSObject subclass is not exposed by default; @objc exposes one member; @objcMembers on a class exposes all its members; dynamic implies @objc; an override of an Objective-C member keeps its Objective-C identity; a member that satisfies an @objc protocol requirement is exposed; and Interface Builder attributes (@IBOutlet, @IBAction, @IBInspectable), @NSManaged and @GKInspectable imply it. Members that Objective-C cannot represent (generics, tuples, Swift-only enums, structs, throws returning something other than a bridgeable value) cannot be marked @objc: the compiler rejects the attribute.

You do not have to guess. swiftc -emit-objc-header (what the -Swift.h file is) writes exactly what Objective-C sees. Reading it is the best audit before changing a public API, and it shows the translation too: String becomes NSString * _Nonnull, an @objc enum becomes NS_ENUM, a Swift init gets OBJC_DESIGNATED_INITIALIZER. Exposure also has a cost: each exposed member gets a selector and an Objective-C thunk, so use @objcMembers sparingly.

The protocol witness, the explicit and dynamic members, the override and the @objcMembers class are exposed. plain, notExposed, the generic, result and the Swift enum are not.

Tuples have no Objective-C equivalent, so the attribute is rejected.

Common traps

  • Adding @objc to a method that returns a tuple or takes an optional Int. It is a compile error; the type cannot be represented.
  • Assuming a method on an NSObject subclass is visible. Check the generated header.

They might follow up with

  • Does private hide a member from Objective-C? An @objc private member is not in the header but is still in the runtime tables.
  • What does @objc(name) do? It chooses the Objective-C name for a class or selector.

Related questions

  • 4.4.1 What do @objc, @objcMembers, @objc(name), dynamic and @nonobjc each do?
  • 4.4.2 Which Swift features cannot be seen from Objective-C?
  • 4.8.1 How do Swift and Objective-C see each other: bridging header, umbrella header, and the -Swift.h file?

How does Swift 6 treat Objective-C types for Sendable and actor… →