В чём разница между addTarget(_:action:for:) и closures для действий кнопок?
addTarget хранит слабую ссылку на target автоматически, но требует @objc и selector. iOS 14+ UIAction/addAction дают type-safe closure API без этих ограничений. В обоих случаях важно избегать retain cycle и дублирования при многократной регистрации.
addTarget(_:action:for:) vs замыкания для кнопок
UIControl исторически использует паттерн Target-Action: объект-получатель (target) и Objective-C-совместимый selector (action). В Swift closure-based подход стал популярной альтернативой, но у каждого есть свои плюсы и ограничения.
Target-Action через addTarget
Классический UIKit API. Target хранится как weak ссылка внутри UIControl, что автоматически защищает от retain cycle.
final class LoginViewController: UIViewController {
private let loginButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
loginButton.addTarget(
self,
action: #selector(loginTapped),
for: .touchUpInside
)
}
@objc private func loginTapped() {
print("Вход выполнен")
}
// Удаление конкретного обработчика
func detach() {
loginButton.removeTarget(self, action: #selector(loginTapped), for: .touchUpInside)
}
}
- Плюсы: нет retain cycle; Objective-C-совместимость; можно передать UIControl и UIEvent в параметрах selector; поддержка target = nil (responder chain).
- Минусы: требует @objc; selector — строка в рантайме (опечатка → crash до iOS 9, с iOS 9 — #selector уменьшает риск, но не устраняет полностью); нельзя захватить произвольный контекст без дополнительного свойства.
Closure-based подход (iOS 14+)
С iOS 14 появился UIAction и addAction(_:for:) — нативный closure API прямо в UIKit.
// iOS 14+ — рекомендуемый closure-способ
let action = UIAction(title: "") { [weak self] _ in
self?.handleLogin()
}
loginButton.addAction(action, for: .touchUpInside)
// Более компактно через конструктор кнопки (iOS 15+)
let button = UIButton(
primaryAction: UIAction { [weak self] _ in
self?.handleLogin()
}
)
Closure через расширение UIControl (iOS 13 и ниже)
До iOS 14 разработчики реализовывали closure API вручную через ассоциированные объекты.
private class ActionHandler {
let action: () -> Void
init(_ action: @escaping () -> Void) { self.action = action }
@objc func invoke() { action() }
}
private var handlerKey = "handlerKey"
extension UIControl {
func addHandler(for events: UIControl.Event, handler: @escaping () -> Void) {
let wrapper = ActionHandler(handler)
addTarget(wrapper, action: #selector(ActionHandler.invoke), for: events)
// Храним wrapper через ассоциированный объект
objc_setAssociatedObject(self, &handlerKey, wrapper, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
// Использование
loginButton.addHandler(for: .touchUpInside) { [weak self] in
self?.handleLogin()
}
Сравнение по ключевым критериям
- Retain cycle: addTarget хранит weak target автоматически; в closures нужно явно писать [weak self].
- Compile-time safety: #selector проверяется компилятором (Swift 2.2+), но метод должен быть @objc; UIAction с замыканием — полностью type-safe.
- Контекст: замыкание может захватить локальные переменные напрямую; addTarget требует хранить контекст в свойствах объекта.
- Удаление: removeTarget/removeAction — у UIAction есть identifier для точечного удаления.
- Поддержка iOS: addTarget — iOS 2+; UIAction — iOS 14+.
Подводные камни
- Retain cycle в замыканиях — если не написать [weak self], замыкание удержит ViewController, и он никогда не освободится. Это самая частая утечка памяти в UIKit-коде.
- Множественные вызовы addTarget — добавление одного и того же target+action несколько раз (например, в viewWillAppear) приводит к многократному вызову handler. UIControl дедуплицирует пары только при одинаковом target+action+event, но если метод другой — нет.
- @objc и минимальный деплоймент — необходимость маркировать приватные методы @objc нарушает инкапсуляцию и увеличивает бинарный размер (objc runtime сохраняет символ).
- Ассоциированные объекты и жизненный цикл — при самодельном closure API через objc_setAssociatedObject хранение нового wrapper перезаписывает старый, что сбрасывает предыдущий обработчик неочевидно.
- UIAction identifier коллизии — если не указать уникальный UIAction.Identifier, при повторном добавлении действие не заменится (в отличие от addTarget).
- removeAllTargets в реиспользуемых ячейках — UITableViewCell переиспользует вью; если не вызвать removeAllTargets() в prepareForReuse, накапливаются обработчики от предыдущих конфигураций.
Common mistakes
- Сводить «
addTarget(_:action:for:)и closures для действий кнопок» к синтаксису и не объяснять main thread. - Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии uikit-21.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «
addTarget(_:action:for:)и closures для действий кнопок» и подтверждает ее корректным примером. - Умеет связать ответ с Auto Layout, тестированием и отладкой на устройстве.
- Называет ограничения подхода uikit-21, включая производительность, память и сопровождение.