Adopting Objective‑C Nullability Annotations in a Mixed Swift/Obj‑C Codebase
Adopt Objective‑C nullability annotations in a mixed Swift/Obj‑C project to improve Swift interop, catch nil bugs at compile time, and preserve runtime behaviour. Follow this step‑by‑step guide to wrap headers, annotate pointers, and verify changes.
16 Oct 2025, 11:30 UTC

Why Nullability Matters
When Swift consumes Objective‑C APIs, the compiler relies on nullability annotations to decide whether a pointer is an Optional or a non‑optional value. Without annotations, every Objective‑C pointer is imported as an implicitly unwrapped optional (AnyObject!), which hides potential nil‑related bugs and forces Swift callers to add defensive code. Adding nonnull and nullable annotations turns those silent failures into compile‑time warnings, improving safety and making the Swift interface clearer.
Desired Outcome
- All public Objective‑C headers expose a precise nullability contract.
- Swift call sites use the correct
Optionalannotations and receive compiler warnings when nil is passed to anonnullparameter. - Legacy runtime behaviour remains unchanged; annotations do not add runtime checks.
Prerequisites
- Project uses Xcode 15 or later with an up‑to‑date Swift compiler.
- Objective‑C source is split into header (.h) and implementation (.m) files.
- Build settings allow adding compiler flags; you have access to the target’s
Build Settings. - Team has a version‑controlled codebase so changes can be reviewed and reverted.
Step‑by‑Step Procedure
- Enable Nullability Diagnostics
In Xcode, open
Build Settingsfor the target and add the following flags toOther C Flags(orOther Swift Flagsif you prefer to see Swift warnings):-Wnullability-warnings -Wnullability-extensionThese flags make the compiler emit warnings when it encounters a nullability annotation that conflicts with the inferred type.
- Wrap Headers with NS_ASSUME_NONNULL
For each public header, add
NS_ASSUME_NONNULL_BEGINat the top andNS_ASSUME_NONNULL_ENDat the bottom. This tells the compiler that, by default, all pointer types arenonnullunless explicitly markednullable.#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface MyFormatter : NSObject @property (nonatomic, strong, null_resettable) NSDateFormatter *dateFormatter; - (NSString *)formatDate:(NSDate *)date; - (nullable NSString *)formatDateString:(NSString *)dateString; @end NS_ASSUME_NONNULL_ENDNotice the
null_resettableproperty: it is non‑nullable for clients but can be set tonilto trigger a default value. Use this sparingly; for most propertiesnonnullis sufficient. - Annotate Ambiguous Pointers
For pointers whose nullability cannot be inferred, use
nullableornull_unspecified. The latter imports as an implicitly unwrapped optional and preserves the old behaviour if you cannot yet decide.- (nullable id)lookupObjectForKey:(NSString *)key; - Build and Verify Swift Import
After editing a header, build the target. In Xcode’s
Report Navigatorlook for warnings that mentionnullability. A typical warning looks like:warning: 'dateFormatter' has no nullability attribute, defaulting to 'nonnull'Open the
Generated Interfacefor the header (right‑click the header → Show Generated Interface) and confirm that the Swift types match your annotations. For example,dateFormattershould appear asNSDateFormatter(non‑optional) andformatDateString:asString?. - Test in Swift
Create a small Swift file that imports the module and calls the annotated APIs:
import MyModule let formatter = MyFormatter() let result = formatter.formatDateString("2026-10-13") // result: String? formatter.dateFormatter = nil // allowed because null_resettableBuild the Swift target. If you passed
nilto anonnullparameter, the compiler will emit an error or warning depending on the-Werrorsetting. - Gradual Roll‑out
Adopt annotations header‑by‑header. After each file, run a full build and run unit tests to confirm no behavioural changes. If a header change breaks many Swift call sites, you can:
- Revert the header to its previous state.
- Mark the problematic pointers as
null_unspecifiedto restore the old implicitly unwrapped behaviour. - Document the change in the commit message for future review.
- Treat Warnings as Errors in New Code
In
Build Settings, setOther C Flagsto include-Werror=nonnullfor new modules. This forces developers to fix nullability issues before merging. Existing legacy modules can remain with warnings until they are fully annotated. - Run Unit Tests
Because annotations do not alter runtime behaviour, existing unit tests should pass unchanged. Run
xcodebuild testfor the target and verify that no new failures appear.
Expected Checks and Recovery
- Compile‑time check: No
nonnullparameter receivesnilin Swift code. The compiler will emit an error if-Werror=nonnullis enabled. - Import check: The Swift generated interface shows
Optionalonly wherenullableis used. - Runtime check: Run existing unit tests to confirm no crashes or behavioural changes.
- Rollback: If a header change introduces many Swift compilation errors, revert the header or mark ambiguous pointers as
null_unspecifiedto preserve the old implicitly unwrapped optional behaviour.
Limitations
- Annotations are purely compile‑time; they do not add runtime nil checks. Existing nil crashes will still occur if callers rely on unexpected nil values.
- Blocks and function pointers require careful placement of the annotation. For example:
void (^ _Nullable completion)(NSError * _Nullable). - Different Xcode/Swift releases may interpret
null_unspecifiedslightly differently; always test against the target toolchain.
Practical Verification Checklist
| Step | What to Verify | Tool/Command |
|---|---|---|
| Build with annotations | No nullability warnings for annotated headers | Run xcodebuild -scheme YourScheme |
| Swift import | Optional types appear only where annotated | Open Generated Interface in Xcode |
| Swift call site | Compiler errors when passing nil to nonnull | Compile Swift file in Xcode |
| Unit tests | All tests pass, no crashes | Run xcodebuild test |
Conclusion
Adding nullability annotations is a low‑risk, high‑benefit change. By wrapping headers with NS_ASSUME_NONNULL_BEGIN/END, annotating only the truly nullable pointers, and enabling strict diagnostics for new code, you make the Swift‑Objective‑C boundary explicit, reduce silent bugs, and provide clearer contracts to future developers.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.