Swift Bridging Headers: Use Objective-C from Swift Projects
Bridging headers let a Swift target see Objective-C declarations, so you can call older APIs, vendor libraries, or mixed-language code from Swift. They are a small but important part of interoperability in Xcode projects.
Quick answer: A bridging header is a special Objective-C header file that you list in your Swift target settings. Any Objective-C headers you import there become visible to Swift code in that target.
Difficulty: Beginner
You'll understand this better if you know: basic Swift files and imports, how Xcode targets work, and the difference between source files and header files.
1. What Is Swift Bridging Headers?
A Swift bridging header is an Objective-C header file that tells the Swift compiler which Objective-C declarations should be available inside a Swift target. It is commonly used in mixed-language apps where Swift code needs to call classes, methods, constants, or functions written in Objective-C.
- It is not a Swift file.
- It is usually named something like ProjectName-Bridging-Header.h.
- It is configured per target, not for the whole project.
- It lets Swift see imported Objective-C headers without manual module wrapping.
2. Why Bridging Headers Matter
Bridging headers matter because many real-world codebases are not written in one language. You may inherit an Objective-C framework, reuse an old utility library, or gradually migrate an app from Objective-C to Swift. A bridging header gives Swift access to that code without requiring you to rewrite everything at once.
They are especially useful when the code you need is not packaged as a Swift module that can be imported directly. In that case, the bridging header becomes the connection point between the two languages.
3. Basic Syntax or Core Idea
A bridging header is just a header file that contains #import statements. Swift does not read Objective-C implementation files directly; it reads declarations exposed through headers.
Minimal example
Suppose your Objective-C header defines a class named LegacyLogger. Your bridging header might look like this:
#import "LegacyLogger.h"After the header is added to the target’s bridging header setting, Swift can use LegacyLogger as if it were available in the module.
In Swift code, that might look like this:
let logger = LegacyLogger()
logger.logMessage("Hello from Swift")The important idea is that the header exposes declarations, while the Swift file uses them normally.
4. Step-by-Step Examples
Example 1: Importing a custom Objective-C class
Start with an Objective-C header that declares a class. The bridging header imports that header so Swift can see the class name and methods.
#import "UserStore.h"Then Swift can create and use the class:
let store = UserStore()
let name = store.currentUserName()This is the most common use case: exposing one or more headers that define app-specific Objective-C APIs.
Example 2: Importing a C-style utility header
Bridging headers can also import headers that declare C functions, constants, and types. Swift can often use those directly after import.
#import "MathUtilities.h"Swift can then call the function:
let result = AddIntegers(4, 7)This works because the compiler can translate many Objective-C and C declarations into Swift-friendly names and types.
Example 3: Importing a third-party Objective-C framework header
When a library is not available as a Swift module, you may need to import its public headers through the bridging header. The exact header name depends on the library.
#import "AnalyticsSDK.h"After that, Swift can call the SDK API:
let tracker = AnalyticsTracker()
tracker.trackEvent("signup")Use this only when the library is actually designed to be included by headers. If the library provides a module, direct import is often cleaner.
Example 4: Accessing imported constants in Swift
Many Objective-C headers define constants or notification names. Once imported, those symbols are available in Swift with translated names.
#import "AppConstants.h"Swift can then use the constant:
let mode = AppDefaultModeThis is useful for shared configuration values, notification identifiers, and legacy global declarations.
5. Practical Use Cases
- Gradually migrating an Objective-C app to Swift without rewriting everything at once.
- Calling legacy helper classes, managers, or utilities from Swift.
- Using a third-party Objective-C library in a Swift target.
- Sharing C APIs, constants, and enums with Swift code.
- Keeping one mixed-language app target instead of splitting code into separate frameworks immediately.
6. Common Mistakes
Mistake 1: Importing Swift files in the bridging header
The bridging header is for Objective-C headers, not Swift source files. New developers sometimes try to connect Swift to Swift this way, but Swift code already sees other Swift files in the same target automatically.
Problem: Importing a Swift file in the bridging header does not work and can lead to build confusion because the compiler expects Objective-C headers there.
#import "MyViewModel.swift"Fix: Remove the Swift import and use the Swift type directly if it is in the same target. If the code is in another module, import that module in the Swift file instead.
// No bridging-header import needed for Swift files in the same target
let viewModel = MyViewModel()The corrected version works because Swift files in the same module are already visible to each other.
Mistake 2: Putting the wrong file path in target settings
The bridging header path must match the file’s location relative to the project or build settings. If it is wrong, Xcode cannot find the file during compilation.
Problem: A bad path can trigger build failures because the compiler never loads the bridging header.
// Example of an incorrect build setting value
NonexistentFolder/Project-Bridging-Header.hFix: Point the target setting to the actual header path, for example ProjectName/ProjectName-Bridging-Header.h if that is where the file lives.
ProjectName/ProjectName-Bridging-Header.hThe corrected path lets the compiler locate and apply the header to the Swift target.
Mistake 3: Importing headers that are not available to the target
Sometimes a header exists in the project, but it is not part of the current target membership or public interface. Swift then reports that it cannot find the declarations you expected.
Problem: If the imported Objective-C header is not visible to the target, Swift cannot see its types and you may get errors such as Cannot find type '...' in scope.
#import "InternalHelper.h"
let helper = InternalHelper()Fix: Make sure the header is part of the correct target, and import a public header that actually declares the type or function.
#import "PublicHelper.h"
let helper = PublicHelper()The corrected version works because Swift can only bridge declarations that are visible through the imported header.
7. Best Practices
Practice 1: Keep the bridging header small
Import only the headers Swift actually needs. A huge bridging header increases compile time and makes dependencies harder to reason about.
#import "UserStore.h"
#import "AppConstants.h"Smaller imports reduce clutter and make it easier to see which Objective-C APIs Swift depends on.
Practice 2: Prefer public headers over implementation details
Only import declarations that are meant to be used by Swift. Avoid reaching into private or internal headers unless you fully control the code and understand the consequences.
// Good: import a public header
#import "NetworkingClient.h"This keeps the mixed-language boundary stable and lowers the chance of breakage during refactors.
Practice 3: Use the bridging header for Objective-C interoperability, not everything
Do not use the bridging header as a catch-all dependency list. If a library provides a proper Swift module, import it in the Swift file that uses it instead of putting it in the bridging header.
import Foundation
// Prefer module imports in Swift when availableThis keeps dependencies explicit and avoids unnecessary coupling between unrelated Swift files and Objective-C headers.
8. Limitations and Edge Cases
- The bridging header is target-specific, so one target’s header does not automatically apply to another target.
- It only helps Swift see declarations from Objective-C or C headers; it does not make Swift declarations visible to Objective-C.
- Import order can matter if one header depends on another, especially in legacy projects with incomplete header hygiene.
- Forward declarations and private headers can produce confusing compiler errors if the actual declarations are not reachable.
- Build times can increase if the bridging header imports too many large headers.
- Some APIs may not map cleanly to Swift because of nullability, macros, or older Objective-C patterns.
9. Practical Mini Project
Here is a small mixed-language example showing how a Swift target can use an Objective-C helper through a bridging header.
First, imagine the Objective-C header declares a simple formatter:
#import <Foundation/Foundation.h>
@interface GreetingFormatter : NSObject
- NSString *formattedGreetingForName: NSString *name;
@endThe bridging header imports that file:
#import "GreetingFormatter.h"Now Swift can use it directly:
let formatter = GreetingFormatter()
let message = formatter.formattedGreeting(forName: "Ava")This project demonstrates the full flow: Objective-C declaration, bridging header import, and Swift usage in the same target.
10. Key Points
- A bridging header exposes Objective-C headers to Swift in the same target.
- It is configured in target build settings, not by adding a Swift import statement.
- Use it for legacy code, C APIs, and Objective-C libraries that are not direct Swift modules.
- Keep it small and import only public declarations that Swift truly needs.
- Most bridging problems come from bad paths, missing headers, or importing the wrong file type.
11. Practice Exercise
- Create a new Swift target in Xcode.
- Add a simple Objective-C header that declares one class method returning a string.
- Set up a bridging header and import that Objective-C header.
- Call the Objective-C method from Swift and print the result.
Expected output: A printed string returned by the Objective-C method.
Hint: Make sure the header is in the correct target membership and that the bridging header path matches the file location.
Here is one complete solution:
// GreetingFormatter.h
#import <Foundation/Foundation.h>
@interface GreetingFormatter : NSObject
- NSString *formattedGreetingForName: NSString *name;
@end
// Project-Bridging-Header.h
#import "GreetingFormatter.h"
// Swift file
let formatter = GreetingFormatter()
let message = formatter.formattedGreeting(forName: "Ava")
print(message)12. Final Summary
Swift bridging headers are the standard way to make Objective-C declarations available inside a Swift target. They are simple in concept: import the headers you need, point the target at the bridging header file, and then use the exposed types and functions from Swift.
Most issues come from setup mistakes rather than the feature itself. If you keep the header small, import only public declarations, and verify the target path carefully, bridging headers provide a reliable path for mixed-language projects and gradual migrations.
As a next step, learn how Swift names are generated from Objective-C declarations and how nullability annotations improve the Swift API that comes through the bridge.