Слабые ссылки на протоколы в Swift без @objc: полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как сделать слабую ссылку на протокол в чистом Swift (без @objc)? Полное руководство с примерами

Представьте: вы пишете приложение на Swift, используете паттерн Delegate, и вдруг — бам! — ваши объекты не уничтожаются, память растёт, а приложение начинает тормозить. Знакомо? Скорее всего, вы столкнулись с retain cycle (циклом сильных ссылок). Классическая проблема, когда делегат держит сильную ссылку на владельца, а владелец — на делегата. Решение, которое приходит в голову первым — сделать ссылку на протокол слабой (weak). Но тут Swift подкидывает сюрприз: вы не можете просто так объявить weak var delegate: MyProtocol, если протокол не помечен @objc. Компилятор выдаст ошибку. Почему? И главное — как это обойти?

В этой статье мы разберём, как сделать weak protocol reference в чистом Swift без @objc. Вы узнаете, почему это вообще проблема, какие есть решения, и как писать красивый, безопасный код без утечек памяти. Поехали!

Почему weak protocol reference не работает «из коробки»?

Давайте вспомним, как работает weak в Swift. Слабая ссылка (weak) может существовать только для reference types (классов). Почему? Потому что только объекты классов имеют управление памятью через ARC (Automatic Reference Counting). Структуры (struct) и перечисления (enum) — это value types, они копируются, и для них понятие «слабой ссылки» не имеет смысла.

Когда вы пишете:

weak var delegate: MyProtocol?

Компилятор не знает, будет ли MyProtocol реализован классом, структурой или перечислением. А вдруг это структура? Тогда weak нельзя применить. Отсюда и ошибка: «'weak' may only be applied to class and class-bound protocol types».

Раньше (в Objective-C) все протоколы по умолчанию могли быть реализованы только классами, поэтому weak работал. В чистом Swift протоколы универсальны — и это прекрасно, но добавляет сложностей.

Решение №1: class-bound protocol (протокол, ограниченный классами)

Самый простой и распространённый способ — явно указать, что протокол может быть реализован только классами. Для этого используется ключевое слово AnyObject (или устаревшее class) в объявлении протокола.

Пример:

protocol MyDelegate: AnyObject {
    func didSomething()
}

class ViewController {
    weak var delegate: MyDelegate?

    func doSomething() {
        delegate?.didSomething()
    }
}

class SomeClass: MyDelegate {
    func didSomething() {
        print("Делегат сработал!")
    }
}

// Использование
let vc = ViewController()
let some = SomeClass()
vc.delegate = some
vc.doSomething() // Выведет: "Делегат сработал!"

Всё работает. MyDelegate теперь может быть реализован только классом (class), а weak — легален. Это рекомендованный Apple подход для делегатов.

Почему это хорошо?

  • Простота и читаемость.
  • Полная поддержка ARC.
  • Нет необходимости в @objc.

Минусы?

  • Вы теряете возможность реализовать протокол структурой. Но для делегатов это обычно и не нужно.

Решение №2: Использование generic wrapper (для экзотических случаев)

Что если вы обязаны разрешить протоколу быть реализованным и классами, и структурами, но при этом хотите слабую ссылку? Такое бывает редко, но возможно. Например, вы пишете библиотеку, где пользователь может передать и класс, и структуру. Тогда на помощь приходит обёртка.

Идея: создать структуру, которая хранит слабую ссылку на объект, реализующий протокол. Для классов — weak, для структур — просто копия.

Пример:

protocol MyProtocol {
    func performAction()
}

struct WeakReference<T: AnyObject> {
    weak var object: T?
}

// Использование для классов
class MyClass: MyProtocol {
    func performAction() {
        print("Класс работает")
    }
}

var ref = WeakReference<MyClass>()
let obj = MyClass()
ref.object = obj
ref.object?.performAction() // "Класс работает"

Этот подход не решает проблему напрямую для протоколов, но позволяет обернуть слабую ссылку. Для полноценного решения потребуется enum с двумя кейсами (для класса и структуры), но это уже оверхед.

Когда использовать: только если вы пишете обобщённый код и не можете ограничить протокол классами. В 99% случаев это избыточно.

Решение №3: @objc протокол (мост в Objective-C)

Если вы не против использовать @objc, это тоже вариант. Протоколы, помеченные @objc, автоматически считаются class-bound (как в старом добром Objective-C).

@objc protocol MyDelegate {
    func didSomething()
}

class ViewController {
    weak var delegate: MyDelegate?
    // ... остальной код
}

Плюсы: работает, просто.

Минусы:

  • Привязка к Objective-C runtime.
  • Не все типы Swift можно использовать (например, enum с associated values).
  • Меньше производительность (но для делегатов это не критично).

Мы не рекомендуем этот путь для нового кода, если нет необходимости в совместимости с Objective-C. Чистый Swift — наше всё.

Как выбрать правильный подход?

Давайте подведём итоги в виде небольшой таблицы решений:

Ситуация Решение Рекомендация
Вы пишете делегат (самый частый случай) Используйте protocol MyDelegate: AnyObject ✅ Лучший выбор
Протокол уже есть, менять его нельзя Используйте обёртку WeakReference или храните ссылку как Optional без weak (но следите за циклами) ⚠️ Только если нет другого выхода
Нужна совместимость с Objective-C @objc protocol ✅ Если код смешанный

Блоги

Рекомендаций по книгам