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

Introduction: Why Refactoring Legacy Code in Swift is a Necessity

Every iOS/macOS developer sooner or later encounters legacy code. This could be a project written in Objective-C and partially migrated to Swift, or an application created in the Swift 2.0 era using Massive View Controller. Such code often works, but maintaining it becomes a nightmare: any change requires hours of testing, and adding a new feature breaks an old one.

Refactoring legacy code in Swift is not just about "tidying up" the code. It is a strategic process that makes the application more testable, extensible, and performant. In this article, we will break down how to approach refactoring correctly, with examples in Swift, so you don't break production and get a working result.



1. Diagnosis: How to Understand That Legacy Code Requires Refactoring

Before changing code, you need to understand what exactly needs to be changed. The main signs of legacy code in Swift:

  • Massive View Controller (MVC anti-pattern) — files with 500+ lines where logic, UI, and network requests are mixed in a single class.
  • Tight coupling — classes directly depend on each other, with no protocols or dependency injection.
  • Lack of tests — if you are afraid to change a line of code because you don't know what will break.
  • Global variables and singletons — using UserDefaults.standard or NotificationCenter.default throughout the project.
  • Force unwrapping — widespread use of ! instead of safe optional unwrapping.


1.1. Tools for Analyzing Legacy Code in Swift

For an objective assessment of the code's state, use tools:

  • SwiftLint — a static analyzer that identifies code style violations.
  • Xcode Instruments — for finding memory leaks and performance issues.
  • Code coverage — shows how much code is covered by tests (usually 0-10% in legacy projects).

Example of typical legacy code that requires refactoring:

class LegacyViewController: UIViewController {        @IBOutlet weak var label: UILabel!    var data: [String] = []        override func viewDidLoad() {        super.viewDidLoad()        // Mix of logic and UI        let url = URL(string: "https://api.example.com/data")!        let data = try! Data(contentsOf: url)        let json = try! JSONSerialization.jsonObject(with: data) as! [[String: Any]]        for item in json {            data.append(item["name"] as! String)        }        label.text = data.first    }}

There are several problems here: force unwrapping, a synchronous network request on the main thread, and a mix of logic and presentation.



2. Strategy for Refactoring Legacy Code in Swift: A Step-by-Step Plan

Refactoring without a strategy is a path to disaster. Use the "combat engineer" approach: first secure the territory, then change the code.



2.1. Step 1: Create a "Safety Net" of Tests

Before changing anything, write tests for the existing code. Use Characterization Tests — tests that capture the current behavior of the system. Even if the code looks terrible, tests will show that you haven't broken the logic.

import XCTest@testable import LegacyApp

class LegacyViewControllerTests: XCTestCase { func testViewDidLoad_SetsLabelText() { let sut = LegacyViewController() sut.loadViewIfNeeded() // Check that label is not nil and contains text XCTAssertNotNil(sut.label.text) }}

This test may not be perfect, but it will capture the current state. After refactoring, the test should pass.



2.2. Step

Blogs

Book Recommendations