Refactoring Legacy Code in Swift: A Step-by-Step Guide

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 Refactor Legacy Code in Swift?



Working with legacy code is an inevitable part of an iOS/macOS developer's life. Projects written in early versions of Swift (2.x, 3.x) or even Objective-C often contain monolithic classes, duplicated logic, and a lack of modularity. Without refactoring, such code becomes a source of bugs and slows down the addition of new features.



In this article, we will break down practical strategies for refactoring legacy code in Swift. You will learn how to safely improve architecture without breaking existing functionality. All examples are applicable to both iOS and macOS applications.



1. Analysis and Preparation: Where to Start?



Before changing the code, it is necessary to understand its current state. Without analysis, you risk breaking working functionality.



1.1. Test Coverage (Characterization Tests)



The first step is to write characterization tests. They capture the current behavior of the code, even if it seems incorrect. Only after this can you safely make changes.



// Example of a legacy function: UserManager.swiftclass UserManager {    func fetchUser(by id: Int) -> String {        // Outdated implementation with force unwrap        let dict = ["id": id, "name": "John"] as [String : Any]        return dict["name"] as! String    }}

// Characterization testimport XCTestclass UserManagerTests: XCTestCase { func testFetchUserReturnsName() { let manager = UserManager() let result = manager.fetchUser(by: 1) XCTAssertEqual(result, "John") }}


1.2. Inventory of "Bad Practices"



Make a list of problematic patterns in the code:

  • Force unwrapping (!) — the main source of crashes.
  • Global singletons — make testing difficult.
  • Magic numbers and strings — reduce readability.
  • Long methods (more than 50 lines) — violate SRP.
  • Lack of types (using Any).


2. Step-by-Step Refactoring: From Simple to Complex



Refactoring legacy code is best done in small steps. Each change should be reversible and accompanied by tests.



2.1. Eliminating Force Unwrapping



Replace ! with safe constructs: guard let or if let.



// Before refactoringclass NetworkService {    func loadData() -> Data {        let url = URL(string: "https://api.example.com")!        return try! Data(contentsOf: url)    }}

// After refactoringclass NetworkService { enum NetworkError: Error { case invalidURL case dataLoadingFailed } func loadData() throws -> Data { guard let url = URL(string: "https://api.example.com") else { throw NetworkError.invalidURL } do { return try Data(contentsOf: url) } catch { throw NetworkError.dataLoadingFailed } }}


2.2. Decomposing "God" Classes



Legacy classes often violate the Single Responsibility Principle (SRP). Break them down into smaller entities.



// Problem: the class does everythingclass ProfileViewController: UIViewController {    func fetchProfile() { /* network request */ }    func parseJSON() { /* parsing */ }    func updateUI() { /* UI update */ }    func saveToCoreData() { /* saving */ }}

// Solution: separation into modulesprotocol ProfileRepository { func fetchProfile(completion: @escaping (Result<Profile, Error>) -> Void)}

class ProfileNetworkRepository: ProfileRepository { func fetchProfile(completion: @escaping (Result<Profile, Error>) -> Void) { // Network logic

Blogs

Book Recommendations