Avatar ·

Как использовать Timer (NSTimer) в Swift: создание, остановка и повторение таймера

Привет! Я только начал изучать Swift и пытаюсь реализовать простой таймер для обратного отсчёта в своём приложении. Мне нужно, чтобы каждую секунду обновлялось значение на экране, а через 10 секунд таймер останавливался. Я нашёл в документации класс Timer, но у меня возникли проблемы. Вот что я пытаюсь сделать: ```swift var seconds = 10 func startTimer() { Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) } @objc func updateTimer() { seconds -= 1 print(seconds) if seconds == 0 { // Как остановить таймер? } } ``` Таймер запускается и отсчитывает секунды, но я не понимаю: 1. Как правильно остановить таймер, когда `seconds` становится 0? 2. Почему Xcode иногда выдаёт предупреждение о сильной ссылке (strong reference cycle)? 3. Как сделать таймер, который срабатывает только один раз (без повторения)? 4. Есть ли способ использовать таймер без селектора, например с замыканием (closure)? Я слышал, что в Swift 3 появились новые способы работы с Timer, но в старых статьях везде пишут про NSTimer и @objc. Подскажите, как правильно и современно работать с таймерами в Swift 5+. Заранее спасибо за помощь! <<>>

1. Основы Timer в Swift

Класс Timer (ранее NSTimer) — это основной способ выполнения кода с задержкой или с повторением. В Swift 5+ рекомендуется использовать современный API с замыканиями, который безопаснее и читабельнее.

2. Создание и остановка таймера (правильный способ)

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

class ViewController: UIViewController {
    var seconds = 10
    var timer: Timer?
    
    func startTimer() {
        // Создаём таймер с замыканием (closure) – современный способ
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
            guard let self = self else { return }
            
            self.seconds -= 1
            print(self.seconds)
            
            if self.seconds == 0 {
                self.stopTimer()
            }
        }
    }
    
    func stopTimer() {
        timer?.invalidate()  // Останавливаем таймер
        timer = nil          // Освобождаем ссылку
        print("Таймер остановлен")
    }
    
    // Не забываем остановить таймер при уходе с экрана
    deinit {
        timer?.invalidate()
    }
}

3. Почему нужно использовать [weak self]

Таймер удерживает сильную ссылку на объект (self), а объект удерживает сильную ссылку на таймер. Это создаёт циклическую ссылку (retain cycle), из-за которой объект никогда не освободится из памяти. [weak self] разрывает этот цикл. Если не использовать weak, ваш ViewController никогда не вызовет deinit.

4. Таймер без повторения (однократный срабатывание)

Просто передайте repeats: false:

Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { timer in
    print("Прошло 5 секунд")
}

5. Таймер с селектором (для совместимости)

Если вы используете селектор (например, для вызова метода из Objective-C рантайма), обязательно добавляйте @objc и сохраняйте ссылку:

class ViewController: UIViewController {
    var timer: Timer?
    
    func startTimer() {
        timer = Timer.scheduledTimer(
            timeInterval: 1.0,
            target: self,
            selector: #selector(updateTimer),
            userInfo: nil,
            repeats: true
        )
    }
    
    @objc func updateTimer() {
        print("Тик")
    }
    
    func stopTimer() {
        timer?.invalidate()
        timer = nil
    }
}

6. Полезные советы

  • Всегда invalidate() таймер, когда он больше не нужен — в viewDidDisappear или deinit.
  • Не используйте таймеры с высокой частотой (менее 0.1 секунды) — для анимаций используйте CADisplayLink.
  • Таймеры не работают в фоновом режиме приложения — для фоновых задач используйте Background Tasks.
  • Точность: Timer не гарантирует миллисекундную точность, особенно при большой нагрузке.

7. Полный рабочий пример

import UIKit

class TimerViewController: UIViewController {
    @IBOutlet weak var label: UILabel!
    
    private var timer: Timer?
    private var remainingSeconds = 10
    
    override func viewDidLoad() {
        super.viewDidLoad()
        startCountdown()
    }
    
    private func startCountdown() {
        label.text = "\(remainingSeconds)"
        
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            guard let self = self else { return }
            
            self.remainingSeconds -= 1
            self.label.text = "\(self.remainingSeconds)"
            
            if self.remainingSeconds == 0 {
                self.timer?.invalidate()
                self.timer = nil
                self.label.text = "Время вышло!"
            }
        }
    }
    
    deinit {
        timer?.invalidate()
    }
}
<<>>

Альтернативный подход: DispatchSourceTimer (более точный и гибкий)

Если вам нужен таймер с высокой точностью или возможностью приостановки/возобновления, используйте DispatchSourceTimer из Grand Central Dispatch. Он работает на уровне системы и не зависит от RunLoop.

class PrecisionTimer {
    private var timer: DispatchSourceTimer?
    
    func start(interval: TimeInterval, repeats: Bool, handler: @escaping () -> Void) {
        let queue = DispatchQueue.global(qos: .background)
        timer = DispatchSource.makeTimerSource(queue: queue)
        
        let deadline = DispatchTime.now()
        let repeatingInterval = repeats ? interval : .never
        
        timer?.schedule(deadline: deadline, repeating: repeatingInterval, leeway: .nanoseconds(0))
        
        timer?.setEventHandler { [weak self] in
            DispatchQueue.main.async {
                handler()
            }
        }
        
        timer?.resume()
    }
    
    func stop() {
        timer?.cancel()
        timer = nil
    }
    
    deinit {
        stop()
    }
}

// Использование:
let timer = PrecisionTimer()
timer.start(interval: 1.0, repeats: true) {
    print("Тик от DispatchSourceTimer")
}

Плюсы и минусы

  • + Высокая точность (до наносекунд)
  • + Можно приостановить (suspend()) и возобновить (resume())
  • + Работает в фоновых потоках без RunLoop
  • - Требует ручного управления памятью (cancel/resume)
  • - Сложнее в использовании для простых задач

Для большинства случаев Timer.scheduledTimer с замыканием — оптимальный выбор. DispatchSourceTimer стоит применять только когда нужна особая точность или работа в фоне.