Implementing Weak Protocol References in Pure Swift: Methods and Best Practices

Dec 11, 2025 · Programming · 13 views · 7.8

Keywords: Swift | Weak References | Protocols | Memory Management | Delegation Pattern

Abstract: This article explores how to implement weak protocol references in pure Swift without using @objc annotation. It explains the mechanism of AnyObject protocol inheritance, the role of weak references in preventing strong reference cycles, and provides comprehensive code examples with memory management best practices. The discussion includes differences between value and reference types in protocols, and when to use weak versus unowned references.

Core Mechanism of Weak Protocol References in Swift

In Swift programming, the weak keyword creates weak references, a crucial mechanism for preventing strong reference cycles (retain cycles). Strong reference cycles occur when two class instances hold strong references to each other, preventing their reference counts from reaching zero and causing memory leaks. However, Swift compiler imposes an important restriction on weak references: they can only be applied to class types, not to value types (such as structs and enums) or protocols not marked as class-only.

Problem Analysis: The Need for AnyObject Inheritance

Consider this code example that demonstrates a common compilation error when attempting to use weak protocol references in pure Swift:

class MyClass {
    weak var delegate: MyClassDelegate?
}

protocol MyClassDelegate {
    // Protocol definition
}

This code produces the compilation error: "weak cannot be applied to non-class type MyClassDelegate". The error occurs because the MyClassDelegate protocol can be adopted by classes, structs, and enums by default, while weak references can only be used with reference types (classes).

Solution: Using AnyObject Protocol Inheritance

The correct solution in pure Swift is to make the protocol inherit from AnyObject:

protocol MyClassDelegate: AnyObject {
    func didCompleteTask()
    func didFailWithError(_ error: Error)
}

class TaskManager {
    weak var delegate: MyClassDelegate?
    
    func performTask() {
        // Task execution logic
        delegate?.didCompleteTask()
    }
}

class ViewController: MyClassDelegate {
    let manager = TaskManager()
    
    init() {
        manager.delegate = self
    }
    
    func didCompleteTask() {
        print("Task completed")
    }
    
    func didFailWithError(_ error: Error) {
        print("Error: ", error.localizedDescription)
    }
}

By making the protocol inherit AnyObject, we explicitly specify that only class types can adopt this protocol. This makes weak var delegate: MyClassDelegate? a valid declaration, as the compiler now knows MyClassDelegate can only be adopted by classes.

Memory Management Principles and Best Practices

weak references play a vital role in Swift memory management:

  1. Avoiding Strong Reference Cycles: When two objects reference each other, at least one reference should be marked as weak. In delegation patterns, the delegate typically doesn't own the delegator, so weak references are appropriate.
  2. Automatic Nil Assignment: weak references are optional types that automatically become nil when the referenced object is deallocated, preventing dangling pointers.
  3. Difference from Unowned: unowned references assume the referenced object always exists and never becomes nil. If the referenced object is unexpectedly deallocated, accessing an unowned reference causes a runtime crash. Therefore, weak is generally safer for delegation patterns.

Practical Application Scenarios and Code Examples

Here's a more comprehensive example demonstrating weak delegation in a network request manager:

protocol NetworkManagerDelegate: AnyObject {
    func didReceiveData(_ data: Data)
    func didReceiveError(_ error: NetworkError)
}

enum NetworkError: Error {
    case invalidURL
    case requestFailed
}

class NetworkManager {
    weak var delegate: NetworkManagerDelegate?
    
    func fetchData(from urlString: String) {
        guard let url = URL(string: urlString) else {
            delegate?.didReceiveError(.invalidURL)
            return
        }
        
        URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard let self = self else { return }
            
            if let error = error {
                DispatchQueue.main.async {
                    self.delegate?.didReceiveError(.requestFailed)
                }
                return
            }
            
            guard let data = data else { return }
            
            DispatchQueue.main.async {
                self.delegate?.didReceiveData(data)
            }
        }.resume()
    }
}

class DataProcessor: NetworkManagerDelegate {
    private let networkManager = NetworkManager()
    
    init() {
        networkManager.delegate = self
    }
    
    func loadData() {
        networkManager.fetchData(from: "https://api.example.com/data")
    }
    
    func didReceiveData(_ data: Data) {
        // Process received data
        print("Received data, size: ", data.count)
    }
    
    func didReceiveError(_ error: NetworkError) {
        // Handle error
        print("Network error: ", error)
    }
}

In this example, NetworkManager holds a weak reference to DataProcessor. When DataProcessor is deallocated, NetworkManager's delegate automatically becomes nil, preventing memory leaks.

Protocol Design Considerations

When designing protocols that require weak references, consider these factors:

  1. Protocol Purpose: If the protocol is primarily for delegation patterns or callback mechanisms, it should typically inherit AnyObject to support weak references.
  2. Type Restrictions: Inheriting AnyObject means structs and enums cannot adopt the protocol. If value type support is needed, consider creating two separate protocol versions.
  3. Swift Version Compatibility: In earlier Swift versions, the class keyword achieved the same functionality (e.g., protocol MyProtocol: class). Since Swift 5.1, AnyObject is recommended as it more accurately expresses intent.

Testing and Verification

To verify weak reference correctness, create test scenarios:

class TestDelegate: MyClassDelegate {
    func didCompleteTask() {
        print("TestDelegate: Task completed")
    }
    
    func didFailWithError(_ error: Error) {
        print("TestDelegate: Error", error)
    }
    
    deinit {
        print("TestDelegate deallocated")
    }
}

func testWeakReference() {
    var taskManager: TaskManager? = TaskManager()
    var testDelegate: TestDelegate? = TestDelegate()
    
    taskManager?.delegate = testDelegate
    
    // Release delegate
    testDelegate = nil
    
    // taskManager.delegate should now be nil
    print("Is delegate nil:", taskManager?.delegate == nil)
    
    taskManager = nil
}

Running this test verifies that when testDelegate is set to nil, taskManager.delegate automatically becomes nil, confirming weak references work correctly.

Conclusion

The correct method for implementing weak protocol references in pure Swift is through protocol inheritance from AnyObject. This approach explicitly restricts protocols to class-only adoption, enabling the use of the weak keyword. Combined with proper memory management practices—such as avoiding strong reference cycles and correctly using weak versus unowned—developers can create robust, memory-leak-free Swift applications. Understanding these concepts is essential for implementing efficient delegation patterns and callback mechanisms.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.