DebounceThrottle 工具類,防抖/節(jié)流控制
僅供參考
-
在 iOS 13 及以上系統(tǒng)使用現(xiàn)代的
Combine實現(xiàn)防抖與節(jié)流; -
在 iOS 12 及以下系統(tǒng)回退使用
GCD(DispatchWorkItem)實現(xiàn)同樣的功能;
通用 DebounceThrottle 類(Combine + GCD 混合實現(xiàn))
import Foundation
import Combine
import UIKit
class DebounceThrottle {
// MARK: - Internal storage
private var debounceCancellable: AnyCancellable?
private var debounceWorkItem: DispatchWorkItem?
private let normalInterval = 0.5
private var lastThrottleDate: Date?
/// 防抖:延遲執(zhí)行,期間再次觸發(fā)將重置
func debounce(for delay: TimeInterval? = nil, action: @escaping () -> Void) {
let delay = (delay == nil ? normalInterval : delay!)
if #available(iOS 13.0, *) {
debounceCancellable?.cancel()
debounceCancellable = Just(())
.delay(for: .seconds(delay), scheduler: RunLoop.main)
.sink { _ in action() }
} else {
debounceWorkItem?.cancel()
let item = DispatchWorkItem(block: action)
debounceWorkItem = item
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: item)
}
}
/// 節(jié)流:一段時間內(nèi)只執(zhí)行一次
func throttle(for interval: TimeInterval? = nil, action: @escaping () -> Void) {
let interval = (interval == nil ? normalInterval : interval!)
let now = Date()
if let last = lastThrottleDate, now.timeIntervalSince(last) < interval {
return // 丟棄
}
lastThrottleDate = now
action()
}
/// 取消防抖(可選)
func cancelDebounce() {
if #available(iOS 13.0, *) {
debounceCancellable?.cancel()
} else {
debounceWorkItem?.cancel()
}
}
deinit {
cancelDebounce()
}
}
用法(無需關(guān)心系統(tǒng)版本)
let debouncer = DebounceThrottle()
// 1. 按鈕點擊防抖
@IBAction func buttonTapped(_ sender: UIButton) {
debouncer.debounce(for: 0.4) { [weak self] in
self?.performAction()
}
}
// 2. 搜索輸入防抖
func searchTextChanged(_ text: String) {
debouncer.debounce(for: 0.3) {
print("搜索:\(text)")
}
}
// 3. 頁面跳轉(zhuǎn)節(jié)流
func goToNextPage(from vc: UIViewController) {
debouncer.throttle(for: 1.0) {
let next = UIViewController()
vc.present(next, animated: true)
}
}
下面是加強版的 DebounceThrottle 工具類,支持多任務(wù)并行防抖/節(jié)流控制。
功能增強版說明
- 支持 iOS 9+,iOS 13+ 使用 Combine
-
支持多任務(wù) ID:你可以給不同的邏輯分配不同的
id,互不干擾 - 支持 debounce 與 throttle
- 接口統(tǒng)一,使用簡單
DebounceThrottle 多任務(wù)支持完整實現(xiàn)
import Foundation
import Combine
/// 多任務(wù)防抖 + 節(jié)流控制器
class DebounceThrottle {
/// 存儲每個任務(wù) ID 對應(yīng)的 debounce 控制器(iOS13+: Combine, iOS12-: DispatchWorkItem)
private var debounceCancellables: [String: Any] = [:]
/// 存儲每個任務(wù) ID 上次 throttle 執(zhí)行時間
private var throttleTimestamps: [String: Date] = [:]
/// 默認間隔時間
private let normalInterval = 0.5
// MARK: - 防抖(Debounce)
/// 延遲執(zhí)行,若在 delay 時間內(nèi)重復(fù)調(diào)用,則重置倒計時,僅執(zhí)行最后一次調(diào)用
/// - Parameters:
/// - id: 用于標(biāo)識任務(wù)的唯一字符串(如:"search"、"clickButton")
/// - delay: 延遲執(zhí)行的時間(秒)
/// - action: 要執(zhí)行的閉包
func debounce(id: String, delay: TimeInterval? = nil, action: @escaping () -> Void) {
let delay = (delay == nil ? normalInterval : delay!)
if #available(iOS 13.0, *) {
// iOS 13+ 使用 Combine
(debounceCancellables[id] as? AnyCancellable)?.cancel()
let cancellable = Just(())
.delay(for: .seconds(delay), scheduler: RunLoop.main)
.sink { _ in
action()
}
debounceCancellables[id] = cancellable
} else {
// iOS 12 及以下使用 GCD
(debounceCancellables[id] as? DispatchWorkItem)?.cancel()
let workItem = DispatchWorkItem(block: action)
debounceCancellables[id] = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
}
}
// MARK: - 節(jié)流(Throttle)
/// 限制一定時間內(nèi)只允許執(zhí)行一次。多次觸發(fā)僅第一次有效。
/// - Parameters:
/// - id: 用于標(biāo)識任務(wù)的唯一字符串
/// - interval: 節(jié)流間隔(秒)
/// - action: 要執(zhí)行的閉包
func throttle(id: String, interval: TimeInterval? = nil, action: @escaping () -> Void) {
let interval = (interval == nil ? normalInterval : interval!)
let now = Date()
if let lastTime = throttleTimestamps[id], now.timeIntervalSince(lastTime) < interval {
// 距離上次執(zhí)行還沒到間隔時間,不執(zhí)行
return
}
throttleTimestamps[id] = now
action()
}
// MARK: - 取消某個防抖任務(wù)(可選)
/// 取消指定 ID 的防抖任務(wù)(如果還沒觸發(fā)就被取消)
func cancelDebounce(id: String) {
if #available(iOS 13.0, *) {
(debounceCancellables[id] as? AnyCancellable)?.cancel()
} else {
(debounceCancellables[id] as? DispatchWorkItem)?.cancel()
}
debounceCancellables.removeValue(forKey: id)
}
// MARK: - 清理所有任務(wù)(可選)
/// 清除所有任務(wù)(可選使用)
func clearAll() {
if #available(iOS 13.0, *) {
debounceCancellables.values.forEach { ($0 as? AnyCancellable)?.cancel() }
} else {
debounceCancellables.values.forEach { ($0 as? DispatchWorkItem)?.cancel() }
}
debounceCancellables.removeAll()
throttleTimestamps.removeAll()
}
deinit {
clearAll()
}
}
示例
let controller = DebounceThrottle()
// 防抖示例(用于按鈕點擊、搜索輸入等)
controller.debounce(id: "search", delay: 0.3) {
print("執(zhí)行搜索請求")
}
// 節(jié)流示例(用于防止重復(fù)跳轉(zhuǎn)、重復(fù)上傳等)
controller.throttle(id: "navigation", interval: 1.0) {
print("頁面跳轉(zhuǎn)")
}
功能總覽
| 方法名 | 功能 | 場景 | 說明 |
|---|---|---|---|
debounce(id:delay:action:) |
防抖 | 輸入搜索、按鈕點擊 | 重復(fù)觸發(fā)僅執(zhí)行最后一次 |
throttle(id:interval:action:) |
節(jié)流 | 頁面跳轉(zhuǎn)、點贊等 | 間隔內(nèi)只執(zhí)行一次 |
cancelDebounce(id:) |
取消防抖任務(wù) | 用于手動中止等待操作 | |
clearAll() |
清理全部任務(wù) | 頁面銷毀時使用 | 防止泄露 |