Rate Limiting в Swift: Ограничение частоты запросов для iOS и macOS

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

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

Начать курс

Rate Limiting в Swift: Полное руководство для iOS и macOS приложений

Rate limiting (ограничение частоты запросов) — критически важный механизм для стабильной работы мобильных и десктопных приложений. В экосистеме Swift (iOS/macOS) этот подход позволяет контролировать количество вызовов API, операций ввода-вывода или любых повторяющихся действий в единицу времени. Без rate limiting ваше приложение рискует быть заблокированным сервером, потратить заряд батареи на бесконечные ретраи или создать чрезмерную нагрузку на сеть.

Зачем нужно ограничение частоты запросов в Swift?

  • Защита от превышения лимитов API — многие серверы (например, GitHub, Twitter, Stripe) устанавливают жесткие квоты на количество запросов в минуту/час.
  • Оптимизация производительности — предотвращение "шторма" вызовов при быстром скролле или анимациях.
  • Экономия трафика и батареи — особенно актуально для мобильных устройств.
  • Соблюдение политик App Store — чрезмерные фоновые запросы могут привести к отклонению приложения.

Основные подходы к реализации Rate Limiting в Swift

Рассмотрим три базовых паттерна, которые можно реализовать без сторонних библиотек:

1. Токен-бакет (Token Bucket)

Классический алгоритм, где "токены" добавляются с фиксированной скоростью. Каждый запрос потребляет один токен. Если токены закончились — запрос отклоняется.

import Foundation

final class TokenBucket {
    private let capacity: Int
    private var tokens: Int
    private let refillRate: Double // токенов в секунду
    private var lastRefill: Date
    private let queue = DispatchQueue(label: "token.bucket.queue")
    
    init(capacity: Int, refillRate: Double) {
        self.capacity = capacity
        self.tokens = capacity
        self.refillRate = refillRate
        self.lastRefill = Date()
    }
    
    func tryConsume() -> Bool {
        queue.sync {
            refill()
            if tokens > 0 {
                tokens -= 1
                return true
            }
            return false
        }
    }
    
    private func refill() {
        let now = Date()
        let elapsed = now.timeIntervalSince(lastRefill)
        let newTokens = Int(elapsed * refillRate)
        if newTokens > 0 {
            tokens = min(capacity, tokens + newTokens)
            lastRefill = now
        }
    }
}

// Пример использования
let limiter = TokenBucket(capacity: 10, refillRate: 2.0) // 10 запросов, 2 в секунду
if limiter.tryConsume() {
    print("✅ Запрос разрешен")
} else {
    print("⛔️ Превышен лимит, ждите")
}

2. Оконный алгоритм (Sliding Window)

Запоминает временные метки последних N успешных запросов. Если окно переполнено — блокирует новые вызовы.

final class SlidingWindowLimiter {
    private let maxRequests: Int
    private let windowDuration: TimeInterval
    private var timestamps: [Date] = []
    private let queue = DispatchQueue(label: "sliding.window.queue")
    
    init(maxRequests: Int, windowDuration: TimeInterval = 60) {
        self.maxRequests = maxRequests
        self.windowDuration = windowDuration
    }
    
    func canProceed() -> Bool {
        queue.sync {
            let now = Date()
            // Удаляем устаревшие метки
            timestamps.removeAll { now.timeIntervalSince($0) > windowDuration }
            
            if timestamps.count < maxRequests {
                timestamps.append(now)
                return true
            }
            return false
        }
    }
}

// Пример: не более 5 запросов за 10 секунд
let windowLimiter = SlidingWindowLimiter(maxRequests: 5, windowDuration: 10)

3. Throttling с DispatchQueue

Использует встроенные механизмы GCD для принудительной задержки между вызовами.

final class Throttler {
    private var workItem: DispatchWorkItem?
    private var lastFire: Date = .distantPast
    private let queue: DispatchQueue
    private let minimumDelay: TimeInterval
    
    init(minimumDelay: TimeInterval, queue: DispatchQueue = .main) {
        self.minimumDelay = minimumDelay
        self.queue = queue
    }
    
    func throttle(_ block: @escaping () -> Void) {
        workItem?.cancel()
        let newItem = DispatchWorkItem { [weak self] in
            self?.lastFire = Date()
            block()
        }
        workItem = newItem
        
        let delay = max(minimumDelay - Date().timeIntervalSince(lastFire), 0)
        queue.asyncAfter(deadline: .now() + delay, execute: newItem)
    }
}

// Использование для поискового поля
let searchThrottler = Throttler(minimumDelay: 0.3) // 300ms задержка
searchThrottler.throttle {
    print("Выполняем поисковой запрос")
}

Популярные библиотеки для Rate Limiting в Swift

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