防抖/节流控制

DebounceThrottle 工具类,防抖/节流控制
仅供参考

  • 在 iOS 13 及以上系统使用现代的 Combine 实现防抖与节流;
  • 在 iOS 12 及以下系统回退使用 GCDDispatchWorkItem)实现同样的功能;

通用 DebounceThrottle 类(Combine + GCD 混合实现)

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?

    /// 防抖:延迟执行,期间再次触发将重置
    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)
        }
    }

    /// 节流:一段时间内只执行一次
    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()
    }
}

用法(无需关心系统版本)

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. 页面跳转节流
func goToNextPage(from vc: UIViewController) {
    debouncer.throttle(for: 1.0) {
        let next = UIViewController()
        vc.present(next, animated: true)
    }
}

下面是加强版的 DebounceThrottle 工具类,支持多任务并行防抖/节流控制

功能增强版说明

  • 支持 iOS 9+,iOS 13+ 使用 Combine
  • 支持多任务 ID:你可以给不同的逻辑分配不同的 id,互不干扰
  • 支持 debounce 与 throttle
  • 接口统一,使用简单

DebounceThrottle 多任务支持完整实现

import Foundation
import Combine

/// 多任务防抖 + 节流控制器
class DebounceThrottle {
    
    /// 存储每个任务 ID 对应的 debounce 控制器(iOS13+: Combine, iOS12-: DispatchWorkItem)
    private var debounceCancellables: [String: Any] = [:]
    
    /// 存储每个任务 ID 上次 throttle 执行时间
    private var throttleTimestamps: [String: Date] = [:]
    
    /// 默认间隔时间
    private let normalInterval = 0.5

    // MARK: - 防抖(Debounce)
    
    /// 延迟执行,若在 delay 时间内重复调用,则重置倒计时,仅执行最后一次调用
    /// - Parameters:
    ///   - id: 用于标识任务的唯一字符串(如:"search"、"clickButton")
    ///   - delay: 延迟执行的时间(秒)
    ///   - action: 要执行的闭包
    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: - 节流(Throttle)

    /// 限制一定时间内只允许执行一次。多次触发仅第一次有效。
    /// - Parameters:
    ///   - id: 用于标识任务的唯一字符串
    ///   - interval: 节流间隔(秒)
    ///   - action: 要执行的闭包
    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 {
            // 距离上次执行还没到间隔时间,不执行
            return
        }
        throttleTimestamps[id] = now
        action()
    }
    
    // MARK: - 取消某个防抖任务(可选)

    /// 取消指定 ID 的防抖任务(如果还没触发就被取消)
    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: - 清理所有任务(可选)

    /// 清除所有任务(可选使用)
    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("执行搜索请求")
}

// 节流示例(用于防止重复跳转、重复上传等)
controller.throttle(id: "navigation", interval: 1.0) {
    print("页面跳转")
}

功能总览

方法名 功能 场景 说明
debounce(id:delay:action:) 防抖 输入搜索、按钮点击 重复触发仅执行最后一次
throttle(id:interval:action:) 节流 页面跳转、点赞等 间隔内只执行一次
cancelDebounce(id:) 取消防抖任务 用于手动中止等待操作
clearAll() 清理全部任务 页面销毁时使用 防止泄露
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容