@selector() в Swift: Полный гайд для ObjC-совместимости

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

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

Начать курс

Что такое @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 runtime
  • dynamic — включает динамическую диспетчеризацию (нужно для KVO)

Для #selector достаточно

Блоги

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