Refactoring Legacy Code in Swift: From A to Z

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Refactoring Legacy Code in Swift: From A to Z

\n

Working with legacy code is an inevitable part of an iOS/macOS developer's life. Often, this is code written in early versions of Swift or even Objective-C, lacking tests, with "magic numbers" and giant UIViewControllers. In this article, we will break down a step-by-step strategy for refactoring such code, turning it into a modern, maintainable, and testable project.

\n\n

1. Preparation and Safety: Before Touching the Code

\n

The most important rule of refactoring: don't break what works. Without tests, any change is a leap into the unknown.

\n
    \n
  • Characterization Tests: Write tests that capture the current behavior. Even if the code seems wrong, first document how it works.
  • \n
  • Version Control System: Commit the current state. Use a feature branch.
  • \n
  • Tools: Xcode with Edit > Refactor (Rename, Extract Method), as well as linters (SwiftLint) to enforce a consistent style.
  • \n
\n\n
// Example of "freezing" behavior for an old method\nclass LegacyCalculator {\n    func calculateTotal(items: [Int]) -> Int {\n        // Old, unreadable logic\n        var result = 0\n        for i in items {\n            result += i * 2\n        }\n        return result + 10\n    }\n}\n\n// Test capturing the behavior\nfunc testCalculateTotal() {\n    let sut = LegacyCalculator()\n    let result = sut.calculateTotal(items: [1, 2, 3])\n    XCTAssertEqual(result, 22) // (1+2+3)*2 + 10 = 22\n}\n
\n\n

2. The "Whipping Boy" Strategy: Global Variables and Singletons

\n

Legacy code is notoriously known for its global states and singletons. This makes the code tightly coupled and unpredictable.

\n

Problem:

\n
class NetworkManager {\n    static let shared = NetworkManager()\n    var baseURL: String = "http://old.api.com"\n    func fetchData() { ... }\n}\n
\n

Solution: Dependency Injection. Create a protocol and pass dependencies through the initializer.

\n
protocol NetworkService {\n    func fetchData()\n}\n\nclass ModernViewModel {\n    private let network: NetworkService\n    \n    init(network: NetworkService) {\n        self.network = network\n    }\n    \n    func load() {\n        network.fetchData()\n    }\n}\n
\n

This approach makes it easy to swap services in tests and makes the code modular.

\n\n

3. Fighting the "Massive View Controller"

\n

A classic iOS legacy: a UIViewController that handles everything — from network requests to UI formatting. Refactoring is done by separating responsibilities (SRP).

\n\n

Step 1: Extract business logic into a separate model (ViewModel).

\n
// Was in the controller\nfunc buttonTapped() {\n    let data = fetchData()\n    let total = data.reduce(0, +)\n    label.text = "Sum: \\(total)"\n}\n\n// Now: ViewModel\nclass MyViewModel {\n    func calculateTotal(data: [Int]) -> String {\n        let total = data.reduce(0, +)\n        return "Sum: \\(total)"\n    }\n}\n
\n\n

Step 2: Move UI setup into separate methods or factories.

\n
    \n
  • Create a ViewBuilder class for complex hierarchies.
  • \n
  • Use UICollectionViewDiffableDataSource instead of the old dataSource.
  • \n
\n\n

4. Modernization: From Objective-C to Swift and New APIs

\n

Legacy code often contains a mix of Objective-C and Swift. When refactoring, it's important to update outdated constructs:

\n\n
    \n
  • Replace NSError with throws: Modern Swift uses exceptions for error handling.
  • \n
  • Eliminate ! (

Blogs

Book Recommendations