Avatar ·

Что такое @selector() в Swift и как его использовать с NSTimer?

Всем привет! Я начинающий iOS-разработчик и перехожу с Objective-C на Swift. Раньше в Objective-C я активно использовал @selector() для работы с таймерами и целевыми действиями (target-action), например: ```objc [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateTimer:) userInfo:nil repeats:YES]; ``` Теперь я пытаюсь сделать то же самое в Swift, но компилятор ругается, когда я пишу: ```swift let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: @selector(updateTimer:), userInfo: nil, repeats: true) ``` Ошибка: `Use of unresolved identifier 'updateTimer'`. Я понимаю, что в Swift что-то изменилось с селекторами, но не могу разобраться, как правильно их объявлять. Пробовал писать `Selector("updateTimer:")` — это работает, но я слышал, что так делать небезопасно и могут быть ошибки во время выполнения. Как правильно определить селектор в Swift? Нужно ли добавлять какие-то специальные атрибуты к методу? И почему компилятор не видит мой метод, хотя он определён в классе? Заранее спасибо за помощь! <<>>

Проблема селекторов в Swift

В Swift селекторы работают иначе, чем в Objective-C. Основная причина вашей ошибки — метод updateTimer: не помечен как доступный для использования с Objective-C runtime. В Swift методы по умолчанию не видны из Objective-C, а селекторы — это механизм именно Objective-C.

Решение: атрибут @objc

Чтобы метод можно было использовать с селектором, его нужно пометить атрибутом @objc. Вот правильный код:

class MyViewController: UIViewController {
    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Правильный способ создания селектора
        timer = Timer.scheduledTimer(timeInterval: 1.0,
                                     target: self,
                                     selector: #selector(updateTimer),
                                     userInfo: nil,
                                     repeats: true)
    }
    
    @objc func updateTimer() {
        print("Таймер сработал!")
    }
    
    deinit {
        timer?.invalidate()
    }
}

Ключевые моменты:

  • #selector вместо @selector — в Swift используется синтаксис #selector(имяМетода)
  • Атрибут @objc — обязателен для любого метода, который будет использоваться как селектор
  • Параметры метода — если метод принимает параметры, их нужно указывать в селекторе иначе

Селекторы с параметрами

Если ваш метод принимает параметр (например, сам таймер), синтаксис меняется:

@objc func updateTimer(_ timer: Timer) {
    print("Таймер сработал: \(timer.timeInterval)")
}

// Создание селектора
let selector = #selector(updateTimer(_:))  // Обратите внимание на (_:)
timer = Timer.scheduledTimer(timeInterval: 1.0,
                             target: self,
                             selector: selector,
                             userInfo: nil,
                             repeats: true)

Почему это безопаснее, чем Selector("updateTimer:")

Использование #selector даёт проверку на этапе компиляции:

  • Если метода не существует — вы получите ошибку компиляции
  • Если метод не помечен как @objc — компилятор тоже укажет на ошибку
  • Если измените сигнатуру метода — компилятор потребует обновить селектор

Строковая инициализация Selector("updateTimer:") не даёт такой защиты и может привести к крашу в рантайме, если метод переименуют или удалят.

Альтернатива: замыкания вместо селекторов

В современном Swift для таймеров можно использовать замыкания, что ещё безопаснее:

timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
    print("Таймер сработал!")
    // self?.updateTimer(timer)
}

Этот подход не требует @objc и полностью типобезопасен.

<<>>

Дополнение: наследование от NSObject

Ещё один важный момент — если ваш класс не наследуется от NSObject или его подкласса (например, UIViewController), то @objc может не сработать. В Swift методы должны быть доступны в Objective-C runtime, а это возможно только для классов, унаследованных от NSObject.

Пример для обычного Swift-класса:

// Неправильно — класс не наследует NSObject
class MyTimerHandler {
    @objc func fire() { }
}

// Правильно
class MyTimerHandler: NSObject {
    @objc func fire() {
        print("Timer fired")
    }
}

// Использование
let handler = MyTimerHandler()
let timer = Timer.scheduledTimer(timeInterval: 1.0,
                                 target: handler,
                                 selector: #selector(MyTimerHandler.fire),
                                 userInfo: nil,
                                 repeats: true)

Совет для UIKit

В большинстве случаев вы работаете с UIViewController, UIView и другими классами UIKit, которые уже наследуют NSObject. Поэтому достаточно просто добавить @objc к методу. Если же вы создаёте свой класс с нуля, не забудьте унаследовать его от NSObject.

Также обратите внимание: начиная с Swift 4, если класс помечен как @objcMembers, все его методы автоматически становятся доступными для селекторов:

@objcMembers class MyViewController: UIViewController {
    func updateTimer() { }  // Уже доступен для #selector
}

Но это избыточно для большинства случаев — лучше явно помечать только нужные методы.