Skip to Content
Course content

141: Objective-C Interop Bridging Rules

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You're going to run into this eventually: you're working on a modern Swift codebase, but there's a piece of "legacy" logic—maybe a complex calculation engine or a specialized storage wrapper—written in Objective-C that works perfectly. Rewriting it is a waste of time, so we bridge it. But bridging isn't magic; it's a set of rules that determine how types on one side of the fence map to types on the other.

Defining our legacy storage class

Let's imagine we have a class called LegacyUserPrefs. It handles some old-school plist persistence. I'll start by writing the Objective-C header. I want to track a username and a favorite color, and maybe a version number.

// LegacyUserPrefs.h
#import <Foundation/Foundation.h>

@interface LegacyUserPrefs : NSObject

@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *favoriteColor;
@property (nonatomic, assign) NSInteger version;

- (void)updateUsername:(NSString *)newName;
- (NSString *)getFormattedPreference:(NSString *)key;

@end

At first glance, this looks fine. But if we just import this into Swift, we're going to run into the "Optionality Problem." Objective-C doesn't have a native concept of non-optionals like Swift does; every object pointer can be nil. Swift, being type-safe, needs to know if these properties can be null or not.

Opening the door with the Bridging Header

To make LegacyUserPrefs visible to Swift, I need a bridging header. If you're using Xcode, it usually asks to create one for you when you add an Objective-C file to a Swift project. If not, you create a ProjectName-Bridging-Header.h file and point to it in your Build Settings.

Inside that header, I just add the import:

#import "LegacyUserPrefs.h"

Now, the Swift compiler scans that header and "translates" the Objective-C declarations into Swift signatures. This is where the bridging rules kick in. NSString becomes String, and NSInteger becomes Int.

The Optionality Surprise

I tried to use the class in my Swift code immediately, and this is where I tripped up. I wrote this:

let prefs = LegacyUserPrefs()
print("Welcome back, \(prefs.username)!") // ❌ Error: Value of optional type 'String?' must be unwrapped

I forgot that because I didn't specify nullability in my Objective-C header, Swift defaults everything to Optional. Since username is an NSString *, Swift sees it as String?. I don't want to be force-unwrapping or optional-binding every single property from this legacy class if I know they'll always have a value.

I need to go back to the Objective-C header and use nonnull and nullable annotations. This is the "modern" way to write Objective-C for Swift interop.

// LegacyUserPrefs.h (Corrected)
#import <Foundation/Foundation.h>

@interface LegacyUserPrefs : NSObject

@property (nonatomic, copy, nonnull) NSString *username; // Swift: String
@property (nonatomic, copy, nullable) NSString *favoriteColor; // Swift: String?
@property (nonatomic, assign) NSInteger version; // Swift: Int (Primitives aren't optional)

- (void)updateUsername:(nonnull NSString *)newName;
- (nullable NSString *)getFormattedPreference:(nonnull NSString *)key;

@end

By adding nonnull to the username, I've told the Swift compiler: "Trust me, this will never be nil." Now, when I go back to Swift, the error vanishes. prefs.username is now a regular String.

Finalizing the Swift integration

Now that the bridging rules are working in our favor, the usage feels native. I can pass Swift strings directly into the Objective-C methods, and Swift handles the conversion to NSString behind the scenes.

let prefs = LegacyUserPrefs()
prefs.username = "DevMentor" // No more optionals here
prefs.favoriteColor = "Midnight Blue" // This is still String?

// This method returns a nullable NSString, so Swift makes it String?
if let formatted = prefs.getFormattedPreference("theme") {
    print("Your theme is \(formatted)")
} else {
    print("No theme set.")
}

One quick tip: if you ever see NSUInteger or NSInteger, they always bridge to UInt and Int. You don't need to worry about nullability for those because they are scalars, not objects. They can't be nil in Objective-C, so they can't be Optional in Swift.




📋 Practical Task

Bridging the NetworkLogger Utility

You have been handed a legacy Objective-C utility class called NetworkLogger. Currently, it is bridging to Swift, but it's creating "Optional Hell" because the original author didn't use nullability annotations. Every single property and method return value is appearing as an Optional in Swift, even though the documentation says they are guaranteed to exist.

Your task: Modify the NetworkLogger.h file provided below to use nonnull and nullable annotations so that the Swift code below it compiles without requiring force-unwraps (!) or optional binding (if let) for the guaranteed values.

// NetworkLogger.h
#import <Foundation/Foundation.h>

@interface NetworkLogger : NSObject

@property (nonatomic, copy) NSString *logDirectory; // Guaranteed to exist
@property (nonatomic, copy) NSString *lastErrorMessage; // Can be nil if no error occurred
@property (nonatomic, assign) NSInteger totalRequests; // Scalar

- (void)logRequest:(NSString *)url; // URL is guaranteed
- (NSString *)getLastLogEntry; // Can be nil if log is empty

@end

Requirement: Ensure that in Swift, logDirectory is a String, lastErrorMessage is a String?, the logRequest argument is a String, and getLastLogEntry returns a String?.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.