Imagine an Objective-C codebase that needs to be translated to Swift without stopping new feature development. We use step-by-step migration: file by file, preserving behavior and not breaking business logic. Full rewrite from scratch is a risky path that loses nuances not documented in the code. Our 5-year experience migrating iOS apps guarantees a smooth transition without downtime. Over 30+ projects, we've developed a process that minimizes regressions and maximizes team productivity.
Why is migration often postponed?
Postponed due to fear: the ObjC/Swift bridging boundary is where nullable/nonnull annotations quietly break, NS_SWIFT_NAME renames confuse, and generic types don't propagate through headers. The fear is understandable: errors at the boundary of two languages appear not immediately but at runtime. However, postponing migration means losing access to new Apple APIs—Swift Concurrency (async/await), SwiftUI, Observation framework. Without migration, either you can't use them or you end up with ugly wrappers. Plus, the Swift compiler catches a category of errors (force unwrap on nil, data race through Sendable) before runtime—this reduces crash rates by an average of 70%.
What does migration bring besides access to new APIs?
The Swift compiler detects up to 70% of potential crashes already at build time—three times more effectively than Objective-C. Additionally, migration simplifies maintenance: code becomes more readable, lines of code decrease by an average of 30%, and using value semantics (struct) reduces bugs related to shared mutable state. In one banking app project (~80,000 lines of ObjC), we reduced crashes by 70% after migration—simply because the compiler started catching force-unwrap errors before runtime. The development team's velocity increased by 25% as a result.
When is it better to migrate and when to wait?
Migration should start if you plan to use new Apple APIs (SwiftUI, async/await), if crash rates are rising, or if the team spends significant time maintaining ObjC code. It can be postponed if the app is stable and doesn't require new features, but this becomes harder each year. Average savings on maintenance after migration amount to $10,000–20,000 per year due to reduced debugging and bug fixing time.
How we approach migration
Step 1: Audit. We build a dependency graph between classes. We look for leaf nodes—classes that depend on nothing (utilities, data models, services). We start with those.
Step 2: Annotations in ObjC headers. Before migrating any class, we set NS_ASSUME_NONNULL_BEGIN/END in .h files, marking nullable where it is actually nullable. This immediately shows where Swift will have Optionals and where not. Skipping this step leads to String? everywhere there should be String.
Step 3: Migrating models. NSObject subclasses with properties become Swift structs (if value semantics fit) or classes (if identity or inheritance is needed). The @objc attribute is only needed where the model is still used from ObjC code—not everywhere.
Step 4: Services and network layer. Completion-handler-based APIs are rewritten to async/await via withCheckedContinuation or withCheckedThrowingContinuation. Old ObjC callback:
func fetchUser(id: String, completion: @escaping (User?, Error?) -> Void) Becomes:
func fetchUser(id: String) async throws -> User ObjC code calling this method continues to work via __attribute__((swift_async(...))) or through an intermediate ObjC wrapper.
Step 5: ViewControllers. The most complex. Here we have IBOutlet, IBAction, delegate patterns, notification observers. We migrate them last, when most dependencies are already in Swift. We move logic into ViewModels (pure Swift), leaving ViewControllers thin.
What pitfalls are encountered?
@objc inflation. After migrating a ViewModel, a developer adds @objc dynamic to a property to support KVO from legacy ObjC code. The Swift compiler then stops type-checking these properties as Swift. Solution: move away from KVO to Combine or @Observable (iOS 17+) and remove @objc dynamic.
Bridging header bloat. A large ProjectName-Bridging-Header.h with dozens of #import slows compilation. As migration progresses, we remove unneeded imports—compilation noticeably speeds up (up to 40% faster).
Tests. ObjC unit tests (XCTest) work in a Swift target unchanged. But if a test tests internal methods of an ObjC class via @testable import, access levels may change when that class migrates. We prepare to adapt tests in parallel with migration.
A mobile banking app, ~80,000 lines of ObjC, team of 3 iOS developers. Migrated in 4 months by priority: first network layer and models (moved to async/await and eliminated callback pyramids), then services (auth, analytics, storage), finally screens. Result: crashes due to ObjC-related exceptions decreased by 70%—simply because the compiler started catching force-unwrap errors earlier at runtime. Source: experience from a banking app migration
Comparison of migration approaches
| Criterion | Full rewrite | Step-by-step migration |
|---|---|---|
| Time | 6–12 months | 2–4 months (depends on size) |
| Risks | High (loss of business logic) | Low (behavior preserved) |
| Parallel new features | No | Yes |
| Testing requirements | Full re-testing | Incremental testing |
What is included in the work
- Codebase audit and creation of a prioritized migration plan
- Adding nullable/nonnull annotations in ObjC headers
- Step-by-step migration: models → services → ViewModels → UI
- Converting completion handlers to async/await
- Adapting unit tests
- Code review and verification of the ObjC/Swift boundary at each stage
- 30-day post-migration support and documentation handover
Timelines
| Codebase size | Estimated timelines | Cost range (USD) |
|---|---|---|
| Up to 10,000 lines ObjC | 2–3 weeks | $3,000–$5,000 |
| 10,000–50,000 lines | 1–2 months | $5,000–$15,000 |
| 50,000+ lines | 2–3 months or more | $15,000–$30,000+ |
Timelines depend on the number of ObjC/Swift boundary points, existence of tests, and team readiness to participate in reviews. Cost is calculated individually after a codebase audit. Request a free audit of your codebase—we will assess the workload and propose a migration plan. Contact us to discuss details.







