Что такое @selector() в Swift и зачем он нужен?
Если вы когда-нибудь работали с Objective-C или сталкивались с устаревшим кодом, то наверняка слышали о магической конструкции @selector(). В Swift она трансформировалась в #selector, но суть осталась прежней: это мост между статической типизацией Swift и динамической природой Objective-C runtime.
Представьте, что вам нужно передать ссылку на метод в качестве аргумента. Например, для обработки нажатия кнопки или подписки на уведомления. В Swift мы привыкли использовать замыкания, но библиотеки Apple (UIKit, AppKit, Foundation) всё ещё полагаются на старый добрый механизм селекторов. Именно здесь @selector() в Swift становится незаменимым инструментом.
В этом руководстве мы разберём:
- Как правильно объявлять селекторы в современном Swift
- Типичные ошибки и способы их избежать
- Реальные примеры из iOS-разработки
- Когда стоит использовать
#selector, а когда — альтернативы
От @selector() к #selector: эволюция синтаксиса
В Objective-C селектор выглядит так:
[@selector(buttonTapped:)]
В Swift 2.0 появилась конструкция #selector, которая стала стандартом. Вот как это выглядит сейчас:
#selector(MyClass.buttonTapped(_:))
Ключевое отличие: #selector в Swift проверяется компилятором. Если вы опечатаетесь в имени метода или передадите неверную сигнатуру, компилятор выдаст ошибку. Это огромный плюс по сравнению с Objective-C, где селектор — просто строка, и ошибка всплывёт только в runtime.
Правила объявления методов для #selector
Метод, который вы передаёте в #selector, должен быть:
- Доступен из Objective-C — помечен атрибутом
@objc - Не приватным (или помечен как
@objc private) - Совместимым по сигнатуре с тем, что ожидает вызывающая сторона
Пример корректного объявления:
class ViewController: UIViewController {
@objc func buttonTapped(_ sender: UIButton) {
print("Кнопка нажата!")
}
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(type: .system)
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
}
Обратите внимание на @objc перед методом. Без него компилятор не позволит использовать #selector.
@selector() Swift: практические примеры
1. Target-Action: кнопки и жесты
Самый частый сценарий использования #selector — это привязка действий к UI-элементам:
class ProfileViewController: UIViewController {
@objc private func avatarTapped(_ gesture: UITapGestureRecognizer) {
print("Аватар нажат")
}
@objc private func settingsButtonPressed() {
print("Открыть настройки")
}
override func viewDidLoad() {
super.viewDidLoad()
// Жест
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(avatarTapped(_:)))
avatarImageView.addGestureRecognizer(tapGesture)
// Кнопка
settingsButton.addTarget(self, action: #selector(settingsButtonPressed), for: .touchUpInside)
}
}
Запомните: если метод принимает параметр (например, UITapGestureRecognizer), вы должны указать _ в сигнатуре селектора: #selector(avatarTapped(_:)).
2. PerformSelector и отложенные вызовы
Иногда нужно вызвать метод динамически, особенно при работе с KVO или таймерами:
class TimerManager {
@objc func fireTimer() {
print("Таймер сработал!")
}
func startTimer() {
// Старый способ (работает, но не рекомендуется)
perform(#selector(fireTimer), with: nil, afterDelay: 2.0)
// Современная альтернатива
Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: false)
}
}
Обратите внимание: performSelector всё ещё жив в Foundation, но Apple рекомендует использовать GCD или Timer напрямую.
3. KVO (Key-Value Observing)
Хотя в Swift 4+ появился NSKeyValueObservation, старый KVO с селекторами всё ещё встречается:
class ObservableObject: NSObject {
@objc dynamic var value: String = ""
}
class Observer {
var observation: NSKeyValueObservation?
@objc func valueChanged(_ change: [NSKeyValueChangeKey: Any]?) {
print("Значение изменилось: \(change?[.newKey] ?? "")")
}
func observe(object: ObservableObject) {
// Старый стиль
object.addObserver(self, forKeyPath: #keyPath(ObservableObject.value),
options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(ObservableObject.value) {
print("Новое значение: \(change?[.newKey] ?? "")")
}
}
}
Советую использовать #keyPath вместо строк — это даёт проверку типов на этапе компиляции.
Типичные ошибки и как их избежать
Ошибка 1: Забыли @objc
Самая распространённая проблема. Если метод не помечен @objc, компилятор выдаст: "Argument of '#selector' refers to a method that is not exposed to Objective-C".
Решение: Добавьте @objc перед методом или наследуйтесь от NSObject.
Ошибка 2: Неправильная сигнатура
Путаница с параметрами — частая головная боль новичков. Например:
// Неправильно: метод без параметра
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
// Правильно: если метод принимает UIButton
@objc func buttonTapped(_ sender: UIButton) { }
Золотое правило: Если вы используете addTarget(_:action:for:), метод должен принимать один параметр — UIButton. Для UITapGestureRecognizer — UITapGestureRecognizer.
Ошибка 3: Путаница с @objc и dynamic
Некоторые думают, что dynamic заменяет @objc. Это не так:
@objc— делает метод видимым для Objective-C runtimedynamic— включает динамическую диспетчеризацию (нужно для KVO)
Для #selector достаточно