Exploring Swizzling in iOS

abdul ahad
5 min readMay 25, 2024

--

Photo by Mel Poole on Unsplash

Swizzling is a technique used in programming, particularly in languages like Objective-C and Swift, to change the behavior of a method at runtime.

This can be useful for various purposes, such as adding logging, debugging, fixing bugs in frameworks, or mocking methods for testing.

How Method Swizzling Works

  1. Objective-C Runtime: Swizzling relies on the Objective-C runtime, which allows for dynamic method resolution. In Swift, you can use Objective-C runtime functions to achieve swizzling.
  2. Method Exchange: Swizzling involves exchanging the implementations of two methods. This means that when the original method is called, the new method is executed instead, and vice versa.

Steps for Method Swizzling

  1. Define the Original and Swizzled Methods: You need to have two methods: the original method you want to replace and the new method (swizzled method) you want to use.
  2. Get Method Implementations: Use the Objective-C runtime to get references to the method implementations.
  3. Exchange Implementations: Swap the implementations of the original method and the swizzled method.

In Swift, which interoperates with Objective-C, this can be achieved by bridging Swift methods to Objective-C using @objc annotations. Once the methods are bridged, you can use functions like class_getInstanceMethod and method_exchangeImplementations to swap the implementations.

Example

Here’s a simple example of how you can write a test using method swizzling in Swift

import XCTest

// Class to be tested
class MyClass {
@objc dynamic func originalMethod() -> String {
return "Original method"
}
}

// Extension to swizzle the method
extension MyClass {
@objc dynamic func swizzledMethod() -> String {
return "Swizzled method"
}
}

class SwizzlingTests: XCTestCase {

fileprivate func doSwizzling() {
// Swizzle methods
let originalMethod = class_getInstanceMethod(MyClass.self, #selector(MyClass.originalMethod))
let swizzledMethod = class_getInstanceMethod(MyClass.self, #selector(MyClass.swizzledMethod))
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}

fileprivate func undoSwizzling() {
// Clean up swizzling
let originalMethod = class_getInstanceMethod(MyClass.self, #selector(MyClass.originalMethod))
let swizzledMethod = class_getInstanceMethod(MyClass.self, #selector(MyClass.swizzledMethod))
method_exchangeImplementations(swizzledMethod!, originalMethod!)
}

// Test method that should use the swizzled implementation
func testSwizzledMethod() {
doSwizzling()
let myObject = MyClass()
XCTAssertEqual(myObject.originalMethod(), "Swizzled method")
undoSwizzling()
}

// Test method that should use the original implementation
func testOriginalMethod() {
let myObject = MyClass()
XCTAssertEqual(myObject.originalMethod(), "Original method")
}
}

In this example:

  • MyClass is the class we want to test. It has a method called originalMethod.
  • We create an extension of MyClass to add a new method called swizzledMethod. This will be used to replace the original method during the test.
  • In the SwizzlingTests class, we override the setUp and tearDown methods to perform method swizzling before and after each test method is called, respectively.
  • We have two test methods: testSwizzledMethod and testOriginalMethod. The first one should use the swizzled implementation, while the second one should use the original implementation.

A Real World Example in iOS

Suppose you have a network layer that fetches data from the internet and you want to test your network layer without making a real request.

// Network Manager
class NetworkManager {
@objc dynamic func fetchData(from url: URL, completion: @escaping (Data?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
completion(data, error)
}
task.resume()
}
}



// Extension to add the swizzled method
extension NetworkManager {
@objc dynamic func swizzled_fetchData(from url: URL, completion: @escaping (Data?, Error?) -> Void) {
let mockData = "Mock data".data(using: .utf8)
completion(mockData, nil)
}
}

// Swizzle function
func swizzleNetworkManager() {
let originalSelector = #selector(NetworkManager.fetchData)
let swizzledSelector = #selector(NetworkManager.swizzled_fetchData)

if let originalMethod = class_getInstanceMethod(NetworkManager.self, originalSelector),
let swizzledMethod = class_getInstanceMethod(NetworkManager.self, swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}


class NetworkManagerTests: XCTestCase {
override func setUp() {
super.setUp()
// Perform method swizzling setup
swizzleNetworkManager()
}

override func tearDown() {
// Clean up after the test
// You may optionally revert the swizzling here
super.tearDown()
}

func testFetchData() {
// Create a network manager instance
let networkManager = NetworkManager()

// Create an expectation to wait for the completion handler
let expectation = self.expectation(description: "Fetch data expectation")

// Call the method that should be swizzled
networkManager.fetchData(from: URL(string: "https://example.com")!) { (data, error) in
// This closure should be invoked with mock data due to swizzling
let mockData = "Mock data".data(using: .utf8)

XCTAssertEqual(mockData,data)


// Fulfill the expectation
expectation.fulfill()
}

// Wait for the expectation to be fulfilled (or timeout after a certain interval)
waitForExpectations(timeout: 5, handler: nil)
}
}

  • Swizzling: Replaces the implementation of fetchData with swizzled_fetchData during tests.
  • Test Setup: Ensures swizzling is set up before each test and verifies that the swizzled method returns the expected mock data.
  • Assertions: Validates that the data returned by fetchData in the test is the mock data defined in swizzled_fetchData.

This setup allows you to test the behavior of NetworkManager without making actual network requests, ensuring your tests are fast and reliable.

What is possible with Swizzling

Modify Behavior of Existing Methods:

  • Swizzling allows you to change or extend the behavior of existing methods at runtime. For example, you can add logging, analytics, or debugging information without changing the original source code.

Override Private Methods:

  • Swizzling can be used to override private methods in a class, allowing you to customize behavior without needing access to the class’s source code.
  • Example: Intercepting private methods in system classes for testing purposes.

Mocking and Testing:

  • Swizzling is useful in unit testing to replace methods with mock implementations, making tests deterministic and isolated from external dependencies.
  • Example: Mocking network requests by sizzling URLSession.

Customizing Third-Party Libraries:

  • If you cannot modify the source code of a third-party library, swizzling provides a way to customize or extend its functionality.
  • Example: Adding additional behaviors to a third-party analytics SDK.

Fix Bugs in Frameworks:

  • Swizzling can be used to patch or fix issues in system frameworks or third-party libraries until an official fix is available.

Benefits and Considerations

Benefits:

  • Testing: Allows for testing code without making real network requests.
  • Debugging: Can add logging or analytics to existing methods without changing their code.
  • Flexibility: Can patch or modify the behavior of third-party libraries or system frameworks.

Considerations:

  • Safety: Swizzling changes method implementations at runtime, which can lead to unexpected behaviors if not done carefully.
  • Maintenance: Can make the code harder to understand and maintain, especially for new developers.
  • Compatibility: Updates to libraries or frameworks might break the swizzled methods.

Conclusion

Swizzling is a powerful tool but should be used with caution and clear documentation

Github: https://github.com/abdahad1996/Swizzling_tests

--

--

abdul ahad

A software developer dreaming to reach the top and also passionate about sports and language learning