Swift didn’t arrive in an empty world. Apple’s platforms are built on decades of Objective-C and C, and huge amounts of valuable code are written in C++. Swift was designed to call into all three directly, and, for Objective-C and since Swift 5.9 for C++, to be called back. Understanding how that bridge works helps you use system APIs well, migrate old projects one file at a time, wrap C libraries safely, and reuse C++ code without writing glue by hand.
Swift Interoperability with Objective-C, C, and C++
Objective-C → Swift: what the importer does
When you import Foundation or import UIKit, you’re using Objective-C APIs that the compiler imports into Swift on the fly. It doesn’t just expose them as-is; it translates them to feel like Swift. Here’s an Objective-C header:
// Bridge.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, PaymentStatus) {
PaymentStatusPending,
PaymentStatusPaid,
PaymentStatusFailed
};
typedef NS_OPTIONS(NSUInteger, ShippingOptions) {
ShippingOptionsExpress = 1 << 0,
ShippingOptionsGiftWrap = 1 << 1
};
@interface LegacyStore : NSObject
- (nullable NSString *)titleForItemWithID:(NSInteger)itemID;
- (BOOL)saveItemNamed:(NSString *)name error:(NSError **)error;
- (void)insertItem:(NSString *)item atIndex:(NSUInteger)index;
@end
NS_ASSUME_NONNULL_END
And here’s how Swift sees it:
// let store = LegacyStore()
//
// let title = store.titleForItem(withID: 1) // String?: `nullable` became an optional
// try store.saveItemNamed("Pen") // BOOL + NSError** became `throws`
// store.insertItem("Pen", at: 0) // "atIndex:" was shortened to "at:"
//
// let status: PaymentStatus = .paid // NS_ENUM became a Swift enum
// let options: ShippingOptions = [.express, .giftWrap] // NS_OPTIONS became an OptionSet
The main translations:
- Names are split into a base name and argument labels, and redundant words are trimmed:
insertItem:atIndex:becomesinsertItem(_:at:), andtitleForItemWithID:becomestitleForItem(withID:). - Nullability annotations decide optionality.
nullablebecomes an optional,nonnull(or anything insideNS_ASSUME_NONNULL_BEGIN/END) becomes non-optional. Unannotated pointers become implicitly unwrapped optionals (String!), the worst of both worlds. - Errors: a method that returns
BOOL(or an object) and takes a trailingNSError **becomes a Swiftthrowsmethod. - Enums and options:
NS_ENUMbecomes a Swift enum,NS_OPTIONSanOptionSet, andNS_ERROR_ENUMan error type you cancatch. - Lightweight generics:
NSArray<NSString *> *imports as[String], while a bareNSArray *imports as[Any]. - Bridged types:
NSString↔String,NSArray↔Array,NSDictionary↔Dictionary,NSNumber↔ numeric types. - Completion handlers become
async(Swift 5.5): a method whose last parameter is a completion block can be called withawait.
Getting your own Objective-C into Swift
In an app target, add a bridging header (MyApp-Bridging-Header.h) and #import the headers Swift should see. In a framework or package, expose headers through the module’s umbrella header or a module map instead; frameworks can’t use bridging headers.
Annotate the headers before importing them: NS_ASSUME_NONNULL_BEGIN/END, nullable where nil is real, and lightweight generics. That single step makes the Swift side dramatically nicer.
Refining the Swift API from Objective-C
Objective-C authors can shape how their API looks in Swift:
- (void)loadWithCompletion:(void (^)(NSData * _Nullable, NSError * _Nullable))completion
NS_SWIFT_NAME(load(completion:));
+ (instancetype)storeWithPath:(NSString *)path NS_SWIFT_NAME(init(path:));
- (void)legacyReset NS_SWIFT_UNAVAILABLE("Use reset(keepingCache:) instead");
- (NSInteger)rawCount NS_REFINED_FOR_SWIFT; // imported as __rawCount, to wrap in a nicer Swift API
Swift → Objective-C
Objective-C can only see Swift declarations it can represent:
- classes that inherit (directly or indirectly) from
NSObject, - members marked
@objc(or every member, with@objcMemberson the class), @objcprotocols, and enums declared@objcwith an integer raw type.
import Foundation
@objc(MYPaymentService)
final class PaymentService: NSObject {
@objc func charge(amount: Double) -> Bool {
amount > 0
}
// Not visible to Objective-C: Decimal and String parameters are fine,
// but a Swift-only type like a struct or a generic couldn't be represented.
func charge(_ amount: Decimal, currency: String) {}
}
print(PaymentService().charge(amount: 10)) // true
Xcode generates a header, MyApp-Swift.h, which Objective-C files #import to use these classes. The @objc(MYPaymentService) attribute sets the name Objective-C sees, which is useful for adding a prefix.
Swift features without an Objective-C equivalent aren’t visible: structs, enums with associated values, generics, tuples, optionals of value types like Int?, and protocols with associated types. The common pattern is a thin @objc wrapper class around Swift-only code.
@objc only exposes a member. dynamic additionally forces calls to go through the Objective-C runtime, which KVO and method swizzling require.
C
Swift imports C headers directly, through a bridging header or a module map. C functions become Swift functions, C structs become Swift structs (with a memberwise initializer), plain C enums become a struct type plus matching global constants (or real Swift enums, when declared with NS_ENUM or enum_extensibility), and pointers become Unsafe…Pointer types:
| C | Swift |
|---|---|
const T * |
UnsafePointer<T> |
T * |
UnsafeMutablePointer<T> |
const void * / void * |
UnsafeRawPointer / UnsafeMutableRawPointer |
T * _Nullable |
Unsafe…Pointer<T>? |
| pointer to an incomplete struct | OpaquePointer |
const char * parameter |
accepts a Swift String directly |
Many C APIs are available just by importing Foundation (which re-exports Darwin):
let squareRoot = sqrt(16.0)
let length = strlen("hello") // a Swift String is passed as `const char *` automatically
print(squareRoot, length) // 4.0 5
C callbacks and context pointers
A common C pattern is a callback function pointer plus a void *context that’s passed back to it. A Swift closure can be used as a C function pointer only if it captures nothing (it becomes a @convention(c) function), so state travels through the context pointer. Unmanaged converts a Swift object to and from that raw pointer:
// C header
typedef void (*progress_callback)(void * _Nullable context, int percent);
void run_job(progress_callback callback, void * _Nullable context);
// final class JobTracker {
// var updates: [Int] = []
// }
//
// let tracker = JobTracker()
// let context = Unmanaged.passUnretained(tracker).toOpaque()
//
// run_job({ context, percent in
// // No captures allowed here: recover the object from the context pointer.
// let tracker = Unmanaged<JobTracker>.fromOpaque(context!).takeUnretainedValue()
// tracker.updates.append(Int(percent))
// }, context)
//
// print(tracker.updates) // [0, 50, 100]
passUnretained doesn’t keep the object alive, so tracker must outlive the job, which is fine here because run_job is synchronous. If the C library calls back later, use passRetained and balance it with exactly one release() when the callbacks are done.
C++ (Swift 5.9)
Since Swift 5.9, Swift can call C++ directly, with no Objective-C++ wrapper in between. Enable it per target: in Xcode, set C++ and Objective-C Interoperability to “C++ / Objective-C++”; in a package, add swiftSettings: [.interoperabilityMode(.Cxx)]. Expose the C++ headers through a module map:
// Geometry.hpp
#include <string>
#include <vector>
struct Point { double x; double y; };
using PointVector = std::vector<Point>;
inline double totalX(const PointVector &points) {
double sum = 0;
for (const auto &p : points) sum += p.x;
return sum;
}
inline std::string greeting(const std::string &name) { return "Hello, " + name; }
// import Geometry
// import CxxStdlib
//
// var points = PointVector()
// points.push_back(Point(x: 1, y: 2))
// points.push_back(Point(x: 3, y: 4))
// print(totalX(points), points.size()) // 4.0 2
//
// let message = greeting(std.string("Sam"))
// print(String(message)) // Hello, Sam
A few practical notes:
- Use C++ type aliases for templates. Swift sees
std::vector’s hidden allocator parameter, so writingstd.vector<Point>in Swift doesn’t work; ausing PointVector = std::vector<Point>;alias in the header does. - C++ value types become Swift structs, copied with their copy constructors. Types that should be shared by reference can be annotated (
SWIFT_SHARED_REFERENCE) to import as reference-counted classes. - Standard library types like
std::string,std::vector,std::map, andstd::optionalare supported, with conversions such asString(cxxString). - Coverage is growing. Not every C++ feature imports yet; check the Swift documentation’s C++ interoperability status for your compiler version.
C++ code can call Swift too, through a generated header, which makes gradual adoption possible in both directions.
Practical advice
- Migrate incrementally. Mixed Objective-C/Swift targets are normal. Convert one class at a time, starting with leaf code nothing else depends on.
- Annotate before importing. Nullability and generics turn
String!and[Any]intoString?and[String]. - Wrap unsafe APIs. Hide C pointers and
Unmanagedbehind small Swift types with safe interfaces. - Keep the boundary narrow. The fewer types cross between languages, the easier both sides are to change.
Interview questions
How does an Objective-C method with an NSError ** parameter appear in Swift?
As a throws method, with the error parameter removed and a BOOL return value dropped.
What happens to unannotated Objective-C pointers?
They import as implicitly unwrapped optionals (T!). Add nullability annotations to get proper optionals or non-optionals.
Can a capturing Swift closure be passed as a C function pointer?
No. C function pointers can’t carry context; pass state through a void * context parameter, typically with Unmanaged.
What can’t Objective-C see from Swift?
Structs, enums with associated values, generics, tuples, and other Swift-only types; only NSObject subclasses and @objc members are exposed.
Summary
- The importer translates Objective-C into idiomatic Swift: names and labels, optionals from nullability,
throwsfromNSError **, enums and option sets,asyncfrom completion handlers. - Swift exposes
NSObjectsubclasses and@objcmembers to Objective-C through a generated header. - C headers import directly; pointers map to
Unsafe…Pointertypes, and callbacks carry state through context pointers. - Since Swift 5.9, Swift calls C++ directly when C++ interoperability is enabled; use type aliases for templates.