Migrating an Old Objective-C Codebase to Swift, Incrementally

We inherited an iOS app originally started in 2013, roughly 140,000 lines of Objective-C with no unit tests on the oldest modules. The client wanted Swift for hiring reasons — Objective-C candidates had become noticeably harder to find — but a full rewrite was a non-starter given the app shipped feature updates every two weeks with real revenue riding on it.
Objective-C and Swift interoperate in the same target through a bridging header, which is what makes incremental migration possible at all. We started new features exclusively in Swift from day one, then picked off existing modules for conversion based on a simple rule: convert a file when you're already touching it for a feature or bug fix, rather than scheduling standalone rewrite sprints that compete with product priorities.
The highest-value early target wasn't the newest code, it was the networking layer — a single AFNetworking-based manager class that every feature depended on. Rewriting it in Swift with Combine, later migrated again to async/await when we raised the deployment target to iOS 15, gave every subsequent Swift module a clean, typed API to call into, rather than each new Swift file having to hand-write its own Objective-C interop boilerplate.
Nullability annotations in the remaining Objective-C code turned out to matter more than expected. Headers without __nullable/__nonnull annotations get bridged into Swift as implicitly unwrapped optionals, which reintroduces the exact crash class Swift is supposed to prevent. We ran a pass adding NS_ASSUME_NONNULL_BEGIN/END blocks across the remaining Objective-C headers, which surfaced about a dozen real nil-handling bugs in code nobody had touched in years.
Eighteen months in, the codebase sits around 55% Swift by line count, concentrated in newer, actively-developed features, with older, stable modules like a legacy PDF export system still in Objective-C because there's no product reason to touch them. That's the honest end state for most incremental migrations — not 100% Swift, but Swift where it earns its keep, with a bridging layer that both languages coexist through indefinitely.

