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:
- 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. - Automatic Nil Assignment:
weakreferences are optional types that automatically becomenilwhen the referenced object is deallocated, preventing dangling pointers. - Difference from Unowned:
unownedreferences assume the referenced object always exists and never becomesnil. If the referenced object is unexpectedly deallocated, accessing anunownedreference causes a runtime crash. Therefore,weakis 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:
- Protocol Purpose: If the protocol is primarily for delegation patterns or callback mechanisms, it should typically inherit
AnyObjectto support weak references. - Type Restrictions: Inheriting
AnyObjectmeans structs and enums cannot adopt the protocol. If value type support is needed, consider creating two separate protocol versions. - Swift Version Compatibility: In earlier Swift versions, the
classkeyword achieved the same functionality (e.g.,protocol MyProtocol: class). Since Swift 5.1,AnyObjectis 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.