Как сделать слабую ссылку на протокол в чистом Swift (без @objc)
Почему weak работает только с классами?
В Swift weak ссылки предназначены для работы с ссылочными типами (классами), так как они используют подсчет ссылок (ARC). Структуры и перечисления — это значимые типы, они копируются при присваивании и не участвуют в ARC. Следовательно, концепция "слабой ссылки" для них не имеет смысла. Именно поэтому компилятор требует, чтобы протокол был ограничен классами.
Правильное решение: наследование от AnyObject
Начиная с Swift 4, рекомендуется использовать AnyObject вместо устаревшего ключевого слова class для ограничения протокола только ссылочными типами. Это делает код более читаемым и соответствует современным стандартам Swift.
Вот правильная реализация:
// 1. Определяем протокол, который может использовать только классы
protocol MyDelegate: AnyObject {
func didSomething()
func didFinishTask()
}
// 2. Класс-владелец, который хранит слабую ссылку на делегата
class TaskManager {
weak var delegate: MyDelegate? // ✅ Теперь работает!
func startTask() {
print("Задача начата")
delegate?.didSomething()
}
func finishTask() {
print("Задача завершена")
delegate?.didFinishTask()
}
}
// 3. Класс-делегат (должен быть классом, а не структурой)
class ViewController: MyDelegate {
let taskManager = TaskManager()
init() {
taskManager.delegate = self // Создаем связь
}
func didSomething() {
print("Делегат: задача начата")
}
func didFinishTask() {
print("Делегат: задача завершена")
}
deinit {
print("ViewController уничтожен")
}
}
// Использование
var vc: ViewController? = ViewController()
vc?.taskManager.startTask() // Выведет: "Задача начата" и "Делегат: задача начата"
vc?.taskManager.finishTask() // Выведет: "Задача завершена" и "Делегат: задача завершена"
vc = nil // Выведет: "ViewController уничтожен" — нет утечки!
Что произойдет без weak?
Если не использовать weak, возникнет retain cycle:
class TaskManager {
var delegate: MyDelegate? // ❌ Сильная ссылка!
}
В этом случае TaskManager держит сильную ссылку на ViewController, а ViewController держит сильную ссылку на TaskManager (через свойство taskManager). Оба объекта никогда не будут уничтожены — это утечка памяти.
Альтернатива: замыкания вместо делегата
Если вам нужно, чтобы делегатом могла быть структура, используйте замыкания:
class TaskManager {
var onStart: (() -> Void)?
var onFinish: (() -> Void)?
func startTask() {
onStart?()
}
}
Замыкания можно захватывать слабо с помощью [weak self], что решает проблему retain cycle.
Ключевые выводы:
- Всегда наследуйте протокол делегата от
AnyObject, чтобы можно было использоватьweak. - Используйте
weak var delegateв классе-владельце для избежания циклических ссылок. - Делегат должен быть классом, так как структуры и перечисления не поддерживают слабые ссылки.
- Проверяйте deinit в классе-делегате, чтобы убедиться, что объекты корректно освобождаются.
Дополнительный подход: использование Associated Object через objc_getAssociatedObject
Если вам абсолютно необходимо хранить слабую ссылку на протокол, который не ограничен классами (например, для структур), можно использовать обертку на основе Objective-C runtime. Это не "чистый" Swift, но иногда полезно:
class WeakBox: AnyObject {
weak var value: AnyObject?
init(_ value: AnyObject?) {
self.value = value
}
}
protocol MyDelegate {
func didSomething()
}
class MyClass {
private var delegateBox: WeakBox?
var delegate: MyDelegate? {
get { delegateBox?.value as? MyDelegate }
set { delegateBox = newValue.map { WeakBox($0 as AnyObject) } }
}
}
Этот способ работает, но имеет недостатки:
- Требуется приведение типов через
as? MyDelegate - Небезопасно для структур — они будут упакованы в
AnyObjectкак объекты Objective-C - Структуры при присваивании копируются, что может привести к неожиданному поведению
Поэтому рекомендую придерживаться первого подхода с AnyObject и классами. Это стандартный и безопасный способ для паттерна делегата в Swift.