UIKitMiddleTechnical

В чём разница между 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, включая производительность, память и сопровождение.

Sources

Related topics