---
title: 'Objective-C Interoperability'
source: 'https://academia.sh/en/courses/ios-platform-and-tools/objective-c-interoperability'
course: 'iOS Platform and Tools'
language: en
updated: '2026-10-21T05:03:10+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that two Swift methods falling onto the same selector are rejected'
    - 'Shows that the names from the bridging header and the generated header merge'
    - 'Measures that a name given with NS_SWIFT_NAME takes precedence over the generated name'
    - 'Separates dynamic dispatch as outside the model'
---

# Objective-C Interoperability

Measures the rejection of two Swift methods that fall onto the same Objective-C selector, the union of the names exposed by the bridging header and the generated header, and the precedence of a name given with NS_SWIFT_NAME; closes the course.

Much of the older code in existing projects is still written in **Objective-C**, and new code is often Swift. The two languages live side by side in the same app. This works because Swift can expose part of its declarations to the Objective-C runtime and can import Objective-C headers. There is a limit in return: Objective-C methods are identified by a name called a **selector**, and two methods in the same class cannot carry the same selector. Swift can also tell methods apart by type information, so the meeting of these two rules has a consequence.

The measurement axis is the same one last time: when two definitions meet, how is it resolved? In this lesson the meeting definitions are selector names, the class names that headers expose, and the Swift names a method receives from two different sources. The lesson is also the close of the course.

## The Classifier

```swift
// Meeting.swift — classifies how two definitions are resolved when they meet
struct Meeting {
    let label: String
    let first: Set<String>
    let second: Set<String>
    let result: Set<String>?

    init(_ label: String, first: Set<String>, second: Set<String>, result: Set<String>?) {
        self.label = label
        self.first = first
        self.second = second
        self.result = result
    }

    var resolution: String {
        if first.isEmpty || second.isEmpty
            || first.isSubset(of: second) || second.isSubset(of: first) {
            return "out-of-scope"
        }
        guard let result, !result.isEmpty else { return "rejection" }
        if result == first.union(second) { return "union" }
        if result == first.intersection(second) { return "intersection" }
        if result == first || result == second { return "precedence" }
        return "off-rule"
    }

    static func list(_ values: Set<String>) -> String {
        values.isEmpty ? "empty" : values.sorted().joined(separator: ",")
    }

    func report() {
        let outcome = result.map(Meeting.list) ?? "none"
        print("\(label): first \(Meeting.list(first)) | second \(Meeting.list(second)) | result \(outcome) -> \(resolution)")
    }
}

func summarize(_ meetings: [Meeting]) {
    for resolution in ["precedence", "intersection", "union", "rejection"] {
        let count = meetings.filter { $0.resolution == resolution }.count
        print("\(resolution): \(count)")
    }
}
```

## The Same Selector

If a Swift class derives from `NSObject` or is marked with `@objc`, its methods are visible to Objective-C. Every visible method gets a selector. The selector is built from the method's name and parameter labels: the method `save(_:)` becomes the selector `save:`, the method `save(name:)` becomes `saveWithName:`, and the parameterless `reset()` becomes `reset`. The real rule also takes prepositions and a few special cases into account; the lesson's model simplifies them and distinguishes only an unlabeled parameter from a labeled one.

A selector carries no parameter type. For this reason two overloads that are legitimate in Swift fall onto the same selector in Objective-C.

```swift
@objc class Store: NSObject {
    @objc func save(_ value: Int) { }
    @objc func save(_ value: String) { }
}
```

These two methods are distinct in Swift because their parameter types differ. The compiler still does not accept this class: both ask for the selector `save:`. We do not print the compiler's rejection; the lesson counts the collision only at the model level. The model builds a selector from each method, compares the selectors of two groups, and if there is a collision leaves the class without exposing any selector. In the second attempt the colliding method is labeled `save(name:)` and the same rule now produces a result.

```swift
// main.swift — the same selector causes two Swift methods to be rejected
struct SwiftMethod {
    let name: String
    let labels: [String]
}

func selector(for method: SwiftMethod) -> String {
    var text = method.name
    for (index, label) in method.labels.enumerated() {
        if index == 0 {
            text += label == "_" ? ":" : "With" + label.prefix(1).uppercased() + label.dropFirst() + ":"
        } else {
            text += (label == "_" ? "" : label) + ":"
        }
    }
    return text
}

func exposed(_ first: Set<String>, _ second: Set<String>) -> Set<String> {
    first.isDisjoint(with: second) ? first.union(second) : []
}

let intSave = SwiftMethod(name: "save", labels: ["_"])
let stringSave = SwiftMethod(name: "save", labels: ["_"])
let namedSave = SwiftMethod(name: "save", labels: ["name"])
let reset = SwiftMethod(name: "reset", labels: [])
let clear = SwiftMethod(name: "clear", labels: [])

let groupA: Set<String> = [selector(for: intSave), selector(for: reset)]
let groupB: Set<String> = [selector(for: stringSave), selector(for: clear)]
let groupC: Set<String> = [selector(for: namedSave), selector(for: clear)]

Meeting("same-selector", first: groupA, second: groupB, result: exposed(groupA, groupB)).report()
Meeting("labelled-overload", first: groupA, second: groupC, result: exposed(groupA, groupC)).report()
```

```
same-selector: first reset,save: | second clear,save: | result empty -> rejection
labelled-overload: first reset,save: | second clear,saveWithName: | result clear,reset,save:,saveWithName: -> union
```

The first meeting is a **rejection**: both groups want the selector `save:`, there is a collision, and the class exposes no selector, so the result is empty. In the second meeting, once the label of the colliding method became `name`, the selector became `saveWithName:`, the collision was gone, and all four selectors were exposed: a **union**. So what causes the rejection is not Swift's overloading but Objective-C's selector, which does not see types. The fix is usually to give one of the methods a label.

**DE21.** The measurement built the selector from the name and the labels. The special rule for prepositions, writing the selector by hand with `@objc(name:)`, and the compiler's real error message were not measured.

## The Bridging Header

Objective-C code is opened to Swift with a **bridging header**. The header imports, in one place, the Objective-C headers that Swift needs to see. The reverse direction is solved by a header the compiler generates: classes exposed in Swift with `@objc` become visible to Objective-C in a generated header named `<ModuleName>-Swift.h`.

```objc
// Bridging-Header.h
#import "LegacyStore.h"
#import "LegacyLogger.h"
```

```objc
// the Objective-C side imports the generated header
#import "App-Swift.h"
```

In a mixed-language module both hold at the same time. The bridging header opens the names `LegacyStore` and `LegacyLogger` to Swift. The generated header opens the names `Router` and `Formatter` to Objective-C. The set of names the module can use in both languages is the union of the names opened by the two headers; no name replaces another header's name outside its own header.

```swift
// main.swift — the names opened by the two headers merge
let bridgingHeader: Set<String> = ["LegacyStore", "LegacyLogger"]
let generatedHeader: Set<String> = ["Router", "Formatter"]
let moduleNames = bridgingHeader.union(generatedHeader)

Meeting(
    "mixed-module",
    first: bridgingHeader,
    second: generatedHeader,
    result: moduleNames
).report()
```

```
mixed-module: first LegacyLogger,LegacyStore | second Formatter,Router | result Formatter,LegacyLogger,LegacyStore,Router -> union
```

Four names came from two headers, and all four are in the module's set. Because the names of the two headers do not intersect, the union and the disjoint set give the same result; the resolution is still **union**. If a class loses its `@objc` marking while the header stays unchanged, the set of the generated header shrinks and the use on the Objective-C side breaks at build time.

**DE22.** The measurement took the names opened by the headers as a set. Members that appear in the generated header but are not opened because they are not marked `@objc`, two headers defining the same name differently, and the order of compilation were not measured.

## A Given Name and Precedence

When Objective-C methods are imported into Swift, the compiler gives them a Swift name by rule. Often the resulting name does not fit Swift's style. The **NS_SWIFT_NAME** attribute in the header lets the developer give the Swift name by hand.

```objc
// LegacyLoader.h
- (void)fetchItemsWithCompletion:(void (^)(NSArray *))completion
    NS_SWIFT_NAME(fetch(then:));
```

There are two names here: `fetchItems(completion:)`, generated by rule, and `fetch(then:)`, written by the developer. Only one of them is visible on the Swift side. The given name replaces the generated name; the two are not visible together. The model chooses the visible name of a method from the given name if there is one, and otherwise from the generated one.

```swift
// main.swift — a name given with NS_SWIFT_NAME takes precedence over the generated name
struct ImportedMethod {
    let generated: String
    let given: String?
    var visible: String { given ?? generated }
}

let loader = ImportedMethod(generated: "fetchItems(completion:)", given: "fetch(then:)")

Meeting(
    "swift-name",
    first: [loader.generated],
    second: [loader.given ?? loader.generated],
    result: [loader.visible]
).report()
```

```
swift-name: first fetchItems(completion:) | second fetch(then:) | result fetch(then:) -> precedence
```

The given name appeared alone: the resolution is **precedence**. The generated name `fetchItems(completion:)` can no longer be written on the Swift side; only `fetch(then:)` is valid. This rule has a cost: Objective-C and Swift code refer to the same method by two names, so the name has to be chosen with everyone on the team who reads both languages in mind.

**DE23.** The measurement took the two names of a single method. A given name that is not a valid Swift name, two methods in the same class receiving the same name, and the old name being kept with a warning were not measured.

## The Bounding Measurement

The last limit is dispatch. When a method is marked `@objc dynamic`, the call is resolved not by Swift's direct call but by the Objective-C runtime's message send. Because an Objective-C class can also override the same selector in a subclass, which body the call goes to is decided at run time. This is not a question of names but a question of choosing a body, and it is not the measure of this course. The axis on which the type determines the body of a call was measured in an earlier course (M08/K02).

To put the question to the model, we write the selector of both sides: the Swift body says `refresh`, and the override in Objective-C also says `refresh`.

```swift
// main.swift — dynamic dispatch is invisible in the name set
let swiftBody: Set<String> = ["refresh"]
let objcOverride: Set<String> = ["refresh"]

Meeting(
    "dynamic-dispatch",
    first: swiftBody,
    second: objcOverride,
    result: swiftBody
).report()
```

```
dynamic-dispatch: first refresh | second refresh | result refresh -> out-of-scope
```

Both sides carry the same selector and the result is **out-of-scope**. Because the sets are the same, the classifier saw no meeting; which body will run cannot be read from these sets.

**DE24.** The measurement counted the selector name of the two bodies. Which body runs, whether the override calls `super`, and the direct dispatch of a call that is not `dynamic` were not measured.

## Summary

- Two Swift methods that fall onto the same Objective-C selector collide; in this case the model rejects the class, and the collision ends when a label changes.
- The bridging header opens names to Swift and the generated header opens names to Objective-C; the names of a mixed-language module are the union of the two sets.
- A name given with `NS_SWIFT_NAME` takes precedence over the name generated by rule.
- Dynamic dispatch is out-of-scope: which body a call goes to is a question of choosing a body, not of names.
- This lesson's resolution: **rejection**.

## Course Wrap-Up

The twelve lessons of this course answered a single question: when two definitions meet, how is it resolved? Each lesson set up a meeting, measured the result, and stated it with one of four names. The table puts the lesson, the meeting, the resolution and the lesson's bounding measurement side by side. The first six lessons cover the structure of the platform, the last six the development environment.

| Lesson | Meeting | Resolution | Bounding measurement |
|---|---|---|---|
| Platform Layers | The four layers of the platform | the core's four (2/2/2/2) | Two nested definitions stay `out-of-scope` |
| Frameworks and Libraries | Framework and library names | union | A qualified name counts as a separate name |
| Domain Frameworks | Domain permissions | intersection | The scope of a permission is not read |
| The App Lifecycle | Lifecycle events | precedence | The order of transitions is not measured |
| Interface Design Guidelines | Guideline and app values | precedence | A guideline is an expectation and stays `off-rule` |
| Versions and Compatibility | Version ranges | intersection | The minimum version is a choice |
| The Development Environment | Two levels of the same setting | precedence | The scheme choice is out-of-scope |
| Project File Structure | Template and build keys | union | Keys are counted, not values |
| The Package Manager | Version constraints | intersection | The set is counted, not the chosen version |
| Simulator and Device | Required and offered capabilities | rejection | Performance is not measured |
| Debugging Basics | Breakpoint sources | union | The order of stepping is not measured |
| Objective-C Interoperability | Methods falling onto the same selector | rejection | Dynamic dispatch is out-of-scope |

The distribution of the eleven lessons other than the opening lesson is as follows: three are precedence (The Development Environment, The App Lifecycle, Interface Design Guidelines), three are intersection (Domain Frameworks, Versions and Compatibility, The Package Manager), three are union (Frameworks and Libraries, Project File Structure, Debugging Basics), and two are rejection (Simulator and Device, Objective-C Interoperability). This count comes from the table's resolution column. The core at the opening of the course, on the other hand, had distributed eight meetings 2/2/2/2. The two are not the same thing: the core counts eight meetings, the table's distribution counts the eleven lessons other than the opening one.

The habit the course leaves is to name first the place where two definitions meet, and then leave the resolution to measurement. The platform, the project and the tools ask the same question from different sides; knowing which set an answer comes from is more reliable than applying a wrong rule by intuition. Next comes the interface built on this platform: M30/K03 covers the declarative interface, the view tree and state.
