¥
立即购买

iOS协议文档生成器

21 浏览
1 试用
0 购买
Dec 5, 2025更新

本提示词专为iOS开发者设计,能够根据协议的核心功能自动生成结构完整、技术准确的协议文档。它采用专业的技术写作风格,确保文档内容清晰、精确且逻辑严密,涵盖协议定义、方法说明、使用示例等关键要素,帮助开发者快速理解和应用iOS开发中的各种协议,提升开发效率和代码质量。

协议概述 该协议集统一定义基于 URLSession 的可重试网络请求策略与回退算法,适用于在弱网或服务短暂不可用时对 REST 客户端的 GET/HEAD 请求进行自动重试。核心能力包括:

  • 指数退避与抖动(Jitter),避免雪崩效应
  • 最大重试次数与总超时控制
  • 幂等性判断,仅对安全方法重试
  • 任务取消与每次尝试超时
  • 错误分类(网络、超时、服务不可用、节流等)
  • 熔断(Circuit Breaker)开关与状态管理
  • 可插拔日志与指标回调(Observability)
  • 前台/后台切换与 Wi‑Fi/蜂窝网络变化协同

协议定义

import Foundation
import Network

// MARK: - 基础类型

/// 应用前后台状态,用于调整策略(例如后台降低重试节奏)
public enum AppState {
    case foreground
    case background
}

/// 当前网络信息(通过 NWPathMonitor 采集)
public struct NetworkInfo {
    public enum LinkType: String {
        case wifi
        case cellular
        case wired
        case other
        case unknown
    }
    public let isReachable: Bool
    public let linkType: LinkType
    public let isExpensive: Bool
    public let isConstrained: Bool

    public init(isReachable: Bool, linkType: LinkType, isExpensive: Bool, isConstrained: Bool) {
        self.isReachable = isReachable
        self.linkType = linkType
        self.isExpensive = isExpensive
        self.isConstrained = isConstrained
    }
}

/// 一次重试流程的上下文信息,用于策略决策与观测
public struct RetryContext {
    public let startTime: Date
    public let appState: AppState
    public let network: NetworkInfo
    public let metadata: [String: String]

    public init(startTime: Date = Date(),
                appState: AppState,
                network: NetworkInfo,
                metadata: [String: String] = [:]) {
        self.startTime = startTime
        self.appState = appState
        self.network = network
        self.metadata = metadata
    }
}

/// 错误分类,供策略判断是否重试与回退
public enum ErrorCategory {
    case transient            // 可恢复的临时错误(如 503, 网络瞬断)
    case throttled            // 429 等节流
    case timeout              // 超时
    case network              // 底层网络错误(如 -1009)
    case server               // 5xx(非明确可恢复)
    case permanent            // 不应重试(4xx、语义错误)
    case cancelled            // 被取消
}

/// 重试决策结果
public struct RetryDecision {
    public let shouldRetry: Bool
    public let delay: TimeInterval
    public let category: ErrorCategory
    public let reason: String

    public init(shouldRetry: Bool, delay: TimeInterval, category: ErrorCategory, reason: String) {
        self.shouldRetry = shouldRetry
        self.delay = delay
        self.category = category
        self.reason = reason
    }
}

// MARK: - 协议:错误分类

/// 将 HTTP 响应与 Error 映射到 ErrorCategory
public protocol ErrorClassifierProtocol {
    func classify(response: HTTPURLResponse?, error: Error?) -> ErrorCategory
}

// MARK: - 协议:退避算法

/// 根据尝试次数与错误类别,计算下一次等待时间
public protocol BackoffStrategy {
    /// 返回下一次尝试前的等待秒数(可以包含抖动)
    func nextDelay(for attempt: Int, category: ErrorCategory, context: RetryContext) -> TimeInterval
    /// 允许策略根据上下文调整最大退避
    var maxDelay: TimeInterval { get }
}

// MARK: - 协议:幂等性判断

/// 判断请求是否幂等,决定是否允许自动重试
public protocol IdempotenceEvaluator {
    func isIdempotent(_ request: URLRequest) -> Bool
}

// MARK: - 协议:熔断器

/// 简化的熔断器接口,控制在大量失败时快速失败
public protocol CircuitBreaker {
    var isOpen: Bool { get }
    func canProceed() -> Bool
    func onSuccess()
    func onFailure(category: ErrorCategory)
}

// MARK: - 协议:观测回调(日志与指标)

/// 可插拔回调,用于接入日志与指标系统
public protocol RetryObserver: AnyObject {
    func willScheduleRetry(request: URLRequest, attempt: Int, delay: TimeInterval, category: ErrorCategory, context: RetryContext, reason: String)
    func didRetry(request: URLRequest, attempt: Int, context: RetryContext)
    func didGiveUp(request: URLRequest, attempts: Int, lastCategory: ErrorCategory, context: RetryContext, reason: String)
    func circuitBreakerChanged(isOpen: Bool, context: RetryContext, reason: String)
}

public extension RetryObserver {
    func willScheduleRetry(request: URLRequest, attempt: Int, delay: TimeInterval, category: ErrorCategory, context: RetryContext, reason: String) {}
    func didRetry(request: URLRequest, attempt: Int, context: RetryContext) {}
    func didGiveUp(request: URLRequest, attempts: Int, lastCategory: ErrorCategory, context: RetryContext, reason: String) {}
    func circuitBreakerChanged(isOpen: Bool, context: RetryContext, reason: String) {}
}

// MARK: - 协议:统一重试策略

/// 统一定义重试策略、退避、超时与开关
public protocol NetworkRetryPolicy {
    /// 最大重试次数(不含首尝试),例如 3 表示最多执行 1 + 3 次
    var maxRetryCount: Int { get }
    /// 单次尝试的超时(覆盖 URLRequest.timeoutInterval),nil 表示不覆盖
    var perAttemptTimeout: TimeInterval? { get }
    /// 本次请求总超时(从第一次发起计时),nil 表示不限制
    var totalTimeout: TimeInterval? { get }
    /// 是否启用熔断器
    var enableCircuitBreaker: Bool { get }
    /// 错误分类器
    var errorClassifier: ErrorClassifierProtocol { get }
    /// 退避策略
    var backoffStrategy: BackoffStrategy { get }
    /// 幂等性判断器
    var idempotenceEvaluator: IdempotenceEvaluator { get }
    /// 观测回调(可选)
    var observer: RetryObserver? { get }

    /// 外部可进一步控制是否重试(在分类与幂等基础上)
    func shouldRetry(request: URLRequest,
                     response: HTTPURLResponse?,
                     data: Data?,
                     error: Error?,
                     attempt: Int,
                     context: RetryContext,
                     category: ErrorCategory) -> Bool
}

方法说明

  • ErrorClassifierProtocol.classify(response:error:)

    • 用途:将 HTTP 响应状态码与底层 Error(例如 URLError)分类为 ErrorCategory,供策略决策。
    • 参数:
      • response:HTTPURLResponse,可为 nil(如底层网络错误)
      • error:Error,可为 nil(如仅有非 2xx 状态码)
    • 返回值:ErrorCategory 枚举,见协议定义。
  • BackoffStrategy.nextDelay(for:category:context:)

    • 用途:计算下一次尝试前的等待时间(支持指数退避与抖动)。
    • 参数:
      • attempt:当前已尝试次数,首个重试为 1
      • category:错误类别
      • context:包含网络/应用态等环境信息
    • 返回值:TimeInterval,单位秒。
  • BackoffStrategy.maxDelay

    • 用途:退避等待时间的上限,用于防止过长阻塞。
  • IdempotenceEvaluator.isIdempotent(_:)

    • 用途:判断请求是否允许自动重试(通常仅 GET/HEAD)。
    • 参数:URLRequest。
    • 返回值:Bool。
  • CircuitBreaker.isOpen / canProceed / onSuccess / onFailure

    • 用途:熔断状态查询与事件上报。
    • 注意:open 状态下 canProceed 返回 false,应快速失败。
  • RetryObserver 回调方法

    • 用途:用于日志与指标系统接入,全部为非必需实现(通过扩展提供默认空实现)。
    • willScheduleRetry:计划重试时回调,包含延迟与原因。
    • didRetry:实际开始某次重试时回调。
    • didGiveUp:达到上限或不可重试时回调。
    • circuitBreakerChanged:熔断状态切换时回调。
  • NetworkRetryPolicy 属性与 shouldRetry

    • maxRetryCount / perAttemptTimeout / totalTimeout / enableCircuitBreaker:全局策略控制。
    • errorClassifier / backoffStrategy / idempotenceEvaluator / observer:可插拔组件。
    • shouldRetry(...):在错误分类与幂等判断后,进一步做业务级判定(例如读取响应头 Retry-After)。

属性说明

  • NetworkRetryPolicy

    • maxRetryCount: Int
    • perAttemptTimeout: TimeInterval?
    • totalTimeout: TimeInterval?
    • enableCircuitBreaker: Bool
    • errorClassifier: ErrorClassifierProtocol
    • backoffStrategy: BackoffStrategy
    • idempotenceEvaluator: IdempotenceEvaluator
    • observer: RetryObserver?
  • BackoffStrategy

    • maxDelay: TimeInterval
  • CircuitBreaker

    • isOpen: Bool

使用示例 以下示例包含:

  • 默认错误分类器
  • 指数退避策略(含抖动)
  • 简化熔断器
  • 默认重试策略
  • 基于 URLSession 的重试 REST 客户端(只自动重试 GET/HEAD)
  • 前台/后台与网络路径监控
import Foundation
import Network
import os.log

// MARK: - 默认错误分类器

public struct DefaultErrorClassifier: ErrorClassifierProtocol {
    public init() {}
    public func classify(response: HTTPURLResponse?, error: Error?) -> ErrorCategory {
        if let urlError = error as? URLError {
            switch urlError.code {
            case .timedOut: return .timeout
            case .notConnectedToInternet, .networkConnectionLost: return .network
            case .cancelled: return .cancelled
            default: return .network
            }
        }
        guard let status = response?.statusCode else {
            return .transient
        }
        switch status {
        case 200..<300: return .transient        // 实际不会进入分类逻辑(成功直接返回)
        case 408:       return .timeout
        case 429:       return .throttled
        case 500:       return .server
        case 502, 503, 504: return .transient
        case 400..<500: return .permanent
        default:        return .server
        }
    }
}

// MARK: - 指数退避策略(带抖动)

public struct ExponentialBackoffStrategy: BackoffStrategy {
    public let base: TimeInterval
    public let multiplier: Double
    public let jitterFraction: Double
    public let maxDelay: TimeInterval

    public init(base: TimeInterval = 0.5,
                multiplier: Double = 2.0,
                jitterFraction: Double = 0.5,
                maxDelay: TimeInterval = 30.0) {
        self.base = base
        self.multiplier = multiplier
        self.jitterFraction = max(0, min(jitterFraction, 1))
        self.maxDelay = maxDelay
    }

    public func nextDelay(for attempt: Int, category: ErrorCategory, context: RetryContext) -> TimeInterval {
        // 根据前后台与网络类型调整基准(后台/蜂窝适度加大退避)
        let envFactor: Double = {
            var f = 1.0
            if context.appState == .background { f *= 1.5 }
            if context.network.linkType == .cellular { f *= 1.3 }
            return f
        }()
        let exp = base * pow(multiplier, Double(max(attempt, 1) - 1)) * envFactor
        let jitterRange = exp * jitterFraction
        let jitter = Double.random(in: 0...jitterRange)
        return min(exp + jitter, maxDelay)
    }
}

// MARK: - 幂等性判断器

public struct DefaultIdempotenceEvaluator: IdempotenceEvaluator {
    public init() {}
    public func isIdempotent(_ request: URLRequest) -> Bool {
        guard let method = request.httpMethod?.uppercased() else { return false }
        return method == "GET" || method == "HEAD"
    }
}

// MARK: - 简化熔断器(半开实现)

public final class SimpleCircuitBreaker: CircuitBreaker {
    private let failureThreshold: Int
    private let recoveryTimeout: TimeInterval
    private var failures: Int = 0
    private var openedAt: Date?
    private var halfOpenProbeAllowed: Bool = true

    public init(failureThreshold: Int = 5, recoveryTimeout: TimeInterval = 30.0) {
        self.failureThreshold = max(1, failureThreshold)
        self.recoveryTimeout = recoveryTimeout
    }

    public var isOpen: Bool {
        if let openedAt = openedAt {
            let elapsed = Date().timeIntervalSince(openedAt)
            if elapsed >= recoveryTimeout {
                // 半开阶段允许一次探测
                return false
            }
            return true
        }
        return false
    }

    public func canProceed() -> Bool {
        if isOpen {
            // 到恢复时间进入半开,限制一次探测
            if openedAt != nil && Date().timeIntervalSince(openedAt!) >= recoveryTimeout && halfOpenProbeAllowed {
                halfOpenProbeAllowed = false
                return true
            }
            return false
        }
        return true
    }

    public func onSuccess() {
        failures = 0
        openedAt = nil
        halfOpenProbeAllowed = true
    }

    public func onFailure(category: ErrorCategory) {
        switch category {
        case .permanent, .cancelled:
            // 永久错误不计入熔断
            return
        default:
            failures += 1
            if failures >= failureThreshold {
                openedAt = Date()
            }
        }
    }
}

// MARK: - 默认重试策略

public struct DefaultNetworkRetryPolicy: NetworkRetryPolicy {
    public let maxRetryCount: Int
    public let perAttemptTimeout: TimeInterval?
    public let totalTimeout: TimeInterval?
    public let enableCircuitBreaker: Bool
    public let errorClassifier: ErrorClassifierProtocol
    public let backoffStrategy: BackoffStrategy
    public let idempotenceEvaluator: IdempotenceEvaluator
    public weak var observer: RetryObserver?

    public init(maxRetryCount: Int = 3,
                perAttemptTimeout: TimeInterval? = 10,
                totalTimeout: TimeInterval? = 60,
                enableCircuitBreaker: Bool = true,
                errorClassifier: ErrorClassifierProtocol = DefaultErrorClassifier(),
                backoffStrategy: BackoffStrategy = ExponentialBackoffStrategy(),
                idempotenceEvaluator: IdempotenceEvaluator = DefaultIdempotenceEvaluator(),
                observer: RetryObserver? = nil) {
        self.maxRetryCount = max(0, maxRetryCount)
        self.perAttemptTimeout = perAttemptTimeout
        self.totalTimeout = totalTimeout
        self.enableCircuitBreaker = enableCircuitBreaker
        self.errorClassifier = errorClassifier
        self.backoffStrategy = backoffStrategy
        self.idempotenceEvaluator = idempotenceEvaluator
        self.observer = observer
    }

    public func shouldRetry(request: URLRequest,
                            response: HTTPURLResponse?,
                            data: Data?,
                            error: Error?,
                            attempt: Int,
                            context: RetryContext,
                            category: ErrorCategory) -> Bool {
        // 幂等性先决条件
        guard idempotenceEvaluator.isIdempotent(request) else { return false }

        // 根据分类判断
        switch category {
        case .transient, .network, .timeout, .throttled:
            break // 倾向重试
        case .server:
            // 5xx 非明确可恢复的,允许有限重试
            break
        case .permanent, .cancelled:
            return false
        }

        // 尊重 Retry-After(秒)或日期
        if let response = response,
           let retryAfterValue = response.value(forHTTPHeaderField: "Retry-After") {
            // 存在该头时,若数值过大可直接放弃,也可选择尊重该值
            if Int(retryAfterValue) ?? 0 > Int(backoffStrategy.maxDelay) {
                return false
            }
        }

        // 背景 + 受限网络下,如果已经多次尝试,可停止
        if context.appState == .background && context.network.isConstrained && attempt >= 2 {
            return false
        }

        return true
    }
}

// MARK: - 观测回调示例(使用 OSLog)

public final class OSLogRetryObserver: RetryObserver {
    private let logger = Logger(subsystem: "com.example.network", category: "retry")

    public init() {}

    public func willScheduleRetry(request: URLRequest, attempt: Int, delay: TimeInterval, category: ErrorCategory, context: RetryContext, reason: String) {
        logger.debug("Schedule retry #\(attempt) in \(delay)s, category=\(String(describing: category)), reason=\(reason)")
    }
    public func didRetry(request: URLRequest, attempt: Int, context: RetryContext) {
        logger.debug("Start retry #\(attempt)")
    }
    public func didGiveUp(request: URLRequest, attempts: Int, lastCategory: ErrorCategory, context: RetryContext, reason: String) {
        logger.error("Give up after \(attempts) attempts, lastCategory=\(String(describing: lastCategory)), reason=\(reason)")
    }
    public func circuitBreakerChanged(isOpen: Bool, context: RetryContext, reason: String) {
        logger.debug("Circuit breaker isOpen=\(isOpen), reason=\(reason)")
    }
}

// MARK: - 重试 REST 客户端(仅自动重试 GET/HEAD)

public final class RetryingRESTClient {
    private let session: URLSession
    private let policy: NetworkRetryPolicy
    private let circuitBreaker: CircuitBreaker?
    private let monitor: NWPathMonitor
    private let monitorQueue = DispatchQueue(label: "network.path.monitor")
    private var latestNetworkInfo = NetworkInfo(isReachable: true, linkType: .unknown, isExpensive: false, isConstrained: false)
    private var appState: AppState = .foreground

    public init(session: URLSession = .shared,
                policy: NetworkRetryPolicy,
                circuitBreaker: CircuitBreaker? = SimpleCircuitBreaker()) {
        self.session = session
        self.policy = policy
        self.circuitBreaker = policy.enableCircuitBreaker ? circuitBreaker : nil
        self.monitor = NWPathMonitor()
        startMonitoring()
        observeAppLifecycle()
    }

    deinit {
        monitor.cancel()
        NotificationCenter.default.removeObserver(self)
    }

    private func startMonitoring() {
        monitor.pathUpdateHandler = { [weak self] path in
            guard let self else { return }
            let type: NetworkInfo.LinkType
            if path.usesInterfaceType(.wifi) {
                type = .wifi
            } else if path.usesInterfaceType(.cellular) {
                type = .cellular
            } else if path.usesInterfaceType(.wiredEthernet) {
                type = .wired
            } else {
                type = .other
            }
            self.latestNetworkInfo = NetworkInfo(
                isReachable: path.status == .satisfied,
                linkType: type,
                isExpensive: path.isExpensive,
                isConstrained: path.isConstrained
            )
        }
        monitor.start(queue: monitorQueue)
    }

    private func observeAppLifecycle() {
        NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
            self?.appState = .background
        }
        NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil) { [weak self] _ in
            self?.appState = .foreground
        }
    }

    /// 发起请求(自动重试仅限幂等方法:GET/HEAD)
    public func data(for request: URLRequest, metadata: [String: String] = [:]) async throws -> (Data, HTTPURLResponse) {
        // 设置每次尝试超时
        var req = request
        if let attemptTimeout = policy.perAttemptTimeout {
            req.timeoutInterval = attemptTimeout
        }

        let context = RetryContext(appState: appState, network: latestNetworkInfo, metadata: metadata)
        let start = context.startTime
        var attempt = 0

        while true {
            // 任务取消快速失败
            try Task.checkCancellation()

            // 熔断器检查
            if let cb = circuitBreaker, !cb.canProceed() {
                policy.observer?.didGiveUp(request: req, attempts: attempt, lastCategory: .permanent, context: context, reason: "Circuit breaker open")
                throw URLError(.cannotConnectToHost, userInfo: [NSLocalizedDescriptionKey: "Circuit breaker open"])
            }

            // 执行请求
            do {
                let (data, response) = try await session.data(for: req)
                guard let http = response as? HTTPURLResponse else {
                    throw URLError(.badServerResponse)
                }

                // 成功(2xx)直接返回
                if (200..<300).contains(http.statusCode) {
                    circuitBreaker?.onSuccess()
                    return (data, http)
                }

                // 非 2xx 分类
                let category = policy.errorClassifier.classify(response: http, error: nil)

                // 总超时检查
                if let totalTimeout = policy.totalTimeout, Date().timeIntervalSince(start) >= totalTimeout {
                    policy.observer?.didGiveUp(request: req, attempts: attempt, lastCategory: category, context: context, reason: "Total timeout exceeded")
                    throw URLError(.timedOut)
                }

                // 是否允许重试
                let canRetry = attempt < policy.maxRetryCount &&
                               policy.shouldRetry(request: req, response: http, data: data, error: nil, attempt: attempt, context: context, category: category)

                guard canRetry else {
                    circuitBreaker?.onFailure(category: category)
                    policy.observer?.didGiveUp(request: req, attempts: attempt, lastCategory: category, context: context, reason: "Retry not allowed or max attempts reached")
                    throw HTTPError(statusCode: http.statusCode, data: data)
                }

                // 计算退避时间(尊重 Retry-After)
                let delayHeader = http.value(forHTTPHeaderField: "Retry-After")
                let delayFromHeader = delayHeader.flatMap { Double($0) }
                let delay = delayFromHeader ?? policy.backoffStrategy.nextDelay(for: attempt + 1, category: category, context: context)

                policy.observer?.willScheduleRetry(request: req, attempt: attempt + 1, delay: delay, category: category, context: context, reason: "HTTP \(http.statusCode)")
                try await sleep(seconds: delay)
                attempt += 1
                policy.observer?.didRetry(request: req, attempt: attempt, context: context)
                continue
            } catch {
                // 底层错误(可能为超时/网络/取消)
                let category = policy.errorClassifier.classify(response: nil, error: error)

                // 取消直接抛出
                if category == .cancelled {
                    policy.observer?.didGiveUp(request: req, attempts: attempt, lastCategory: category, context: context, reason: "Task cancelled")
                    throw error
                }

                // 总超时检查
                if let totalTimeout = policy.totalTimeout, Date().timeIntervalSince(start) >= totalTimeout {
                    policy.observer?.didGiveUp(request: req, attempts: attempt, lastCategory: category, context: context, reason: "Total timeout exceeded")
                    throw URLError(.timedOut)
                }

                // 是否允许重试
                let canRetry = attempt < policy.maxRetryCount &&
                               policy.shouldRetry(request: req, response: nil, data: nil, error: error, attempt: attempt, context: context, category: category)

                guard canRetry else {
                    circuitBreaker?.onFailure(category: category)
                    policy.observer?.didGiveUp(request: req, attempts: attempt, lastCategory: category, context: context, reason: "Retry not allowed or max attempts reached")
                    throw error
                }

                // 退避并重试
                let delay = policy.backoffStrategy.nextDelay(for: attempt + 1, category: category, context: context)
                policy.observer?.willScheduleRetry(request: req, attempt: attempt + 1, delay: delay, category: category, context: context, reason: "\(error)")
                try await sleep(seconds: delay)
                attempt += 1
                policy.observer?.didRetry(request: req, attempt: attempt, context: context)
                continue
            }
        }
    }

    private func sleep(seconds: TimeInterval) async throws {
        try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
    }

    /// 自定义错误,用于封装非 2xx 的 HTTP 响应
    public struct HTTPError: Error {
        public let statusCode: Int
        public let data: Data?
    }
}

// MARK: - 使用示例

// 初始化策略与客户端
let observer = OSLogRetryObserver()
let policy = DefaultNetworkRetryPolicy(
    maxRetryCount: 3,
    perAttemptTimeout: 8,
    totalTimeout: 45,
    enableCircuitBreaker: true,
    observer: observer
)
let client = RetryingRESTClient(policy: policy)

// 发起一个 GET 请求(自动重试)
func fetchExample() async {
    var req = URLRequest(url: URL(string: "https://api.example.com/resource")!)
    req.httpMethod = "GET"
    do {
        let (data, response) = try await client.data(for: req, metadata: ["requestId": UUID().uuidString])
        // 处理 data/response
        print("Success: \(response.statusCode), bytes=\(data.count)")
    } catch {
        print("Failed: \(error)")
    }
}

注意事项

  • 平台与并发

    • 上述示例使用 URLSession 的异步 API(data(for:)),要求 iOS 15+。在更低版本需使用 completionHandler 并桥接到 async/await。
    • 取消支持依赖 Task 取消。调用方取消 Task 时,client 会快速失败。
  • 幂等性与方法范围

    • 默认仅对 GET/HEAD 自动重试。对非幂等方法(POST/PUT/PATCH/DELETE)应避免自动重试,或仅在服务端确保幂等语义(如幂等键)后再启用。
  • 超时与重试次数

    • perAttemptTimeout 会覆盖 URLRequest.timeoutInterval。
    • totalTimeout 以首尝试起计,超过后不再重试;需根据业务 SLA 与弱网表现合理设置。
  • 退避与抖动

    • 指数退避配合抖动能有效降低“惊群”。可根据前后台与网络类型调整退避幅度。
    • 若服务端返回 Retry-After,将优先尊重该值(秒),避免过度请求。
  • 错误分类

    • 分类器示例对常见 URLError 与状态码做了合理映射:429/503/504/网络瞬断倾向重试;4xx 永久错误不重试。
    • 可根据业务扩展(如特定 5xx/响应体错误码)。
  • 熔断器

    • 当连续失败达到阈值,将进入 open 状态直接失败;恢复时间后进入 half-open,允许一次探测。
    • 可按服务稳定性调节 failureThreshold 与 recoveryTimeout。不要对永久错误计入熔断。
  • 前后台与网络变化

    • 通过 UIApplication 通知与 NWPathMonitor 更新上下文,策略可在后台或蜂窝网络上加大退避或提前停止。
    • iOS 在后台对网络有更多限制,如需后台长期请求,请结合后台任务(BGTaskScheduler)或后台传输会话(URLSessionConfiguration.background)。
  • 指标与日志

    • 通过 RetryObserver 接入现有观测系统,记录重试次数、等待时间、放弃原因与熔断状态。
    • 在高并发场景下注意日志采样与限流,避免二次放大压力。
  • 线程安全

    • NWPathMonitor 回调在自定义队列,更新共享状态时避免数据竞争。示例中使用私有队列与只读访问以降低复杂度。
  • 安全与合规

    • 避免对昂贵网络(isExpensive=true)进行过度重试,尤其在蜂窝网络环境。
    • 确保遵守服务端速率限制与客户端功耗预算。

协议概述 该协议族用于规范 iOS 应用中图片缓存与获取流程,统一组件间的图片读取、写入、预取、失效与解码策略,目标是:

  • 两级缓存:优先命中内存缓存,其次命中磁盘缓存,未命中再走网络(由上层调用方实现)
  • TTL 和容量控制:支持全局与条目级 TTL;按容量(字节)淘汰
  • 统一键生成:可配置的 Key 生成规则,避免重复下载
  • 并发安全:协议要求线程安全(Sendable),默认采用 async/await
  • 占位与预取:提供占位图获取与滚动场景的预取 API
  • 解码选项扩展点:支持下采样等解码策略的扩展

适用场景:Feed 流与详情页的图片加载,减少重复下载,并在快速滑动时确保高命中与主线程流畅。

协议定义 以下为一组互补的协议与配套类型,覆盖键生成、解码选项、缓存层与统一获取接口。

import UIKit

// MARK: - 基础类型

/// 磁盘/内存命中来源
public enum ImageSource {
    case memory
    case disk
    case network
}

/// 失效与清理的目标范围
public enum CacheScope {
    case memory
    case disk
    case all
}

/// 解码选项(可扩展)
public struct ImageDecodingOptions: Sendable, Equatable {
    public enum ContentMode: Sendable {
        case fill
        case fit
    }
    public var downsampleToPixelSize: CGSize? // 若提供,则进行下采样解码
    public var scale: CGFloat                 // 目标屏幕 scale
    public var contentMode: ContentMode       // 下采样适配策略

    public init(downsampleToPixelSize: CGSize? = nil,
                scale: CGFloat = UIScreen.main.scale,
                contentMode: ContentMode = .fit) {
        self.downsampleToPixelSize = downsampleToPixelSize
        self.scale = scale
        self.contentMode = contentMode
    }
}

/// 请求模型:统一承载缓存与解码所需参数
public struct ImageRequest: Sendable, Hashable {
    public var url: URL
    public var overrideKey: String?              // 若提供则使用此 Key
    public var decodingOptions: ImageDecodingOptions
    public var cachePolicy: CachePolicy          // 读取缓存的策略
    public var userInfo: [String: AnyHashable]?  // 业务透传

    public init(url: URL,
                overrideKey: String? = nil,
                decodingOptions: ImageDecodingOptions = .init(),
                cachePolicy: CachePolicy = .default,
                userInfo: [String: AnyHashable]? = nil) {
        self.url = url
        self.overrideKey = overrideKey
        self.decodingOptions = decodingOptions
        self.cachePolicy = cachePolicy
        self.userInfo = userInfo
    }
}

/// 读取策略(示例)
public enum CachePolicy: Sendable {
    case `default`                 // 先内存->磁盘;过期则跳过
    case reloadIgnoringMemory      // 忽略内存,查磁盘
    case reloadIgnoringCache       // 忽略缓存,走网络(交由上层处理)
    case returnCacheElseLoad       // 有就返回,无则上层走网络
}

/// 统一返回
public struct ImageLoadResult: Sendable {
    public let image: UIImage
    public let source: ImageSource
    public let key: String
}

// MARK: - 键生成

/// 图片缓存键生成
public protocol ImageKeyProviding: Sendable {
    /// 根据请求生成稳定且唯一的 Key。
    /// 注意:Key 应当包含影响像素内容的参数(如 URL、尺寸、解码选项等),避免“不同显示需求命中同一文件”的错误复用。
    func makeKey(for request: ImageRequest) -> String
}

// MARK: - 缓存协议(两级缓存)

/// 两级缓存(内存 + 磁盘),具备 TTL 与容量控制。
/// 线程安全要求:实现类型必须是并发安全的(推荐使用 actor/锁),并符合 Sendable。
public protocol ImageCache: Sendable {
    // 配置
    var memoryCapacityBytes: Int { get set }    // 内存容量上限(字节)
    var diskCapacityBytes: Int { get set }      // 磁盘容量上限(字节)
    var defaultTTL: TimeInterval { get set }    // 默认 TTL(秒)

    var keyProvider: ImageKeyProviding { get }

    // 读取
    func image(forKey key: String,
               decoding: ImageDecodingOptions?) async -> UIImage?

    // 写入
    /// - Parameters:
    ///   - image: 已解码图片,建议写入内存。
    ///   - originalData: 原始编码数据(若有则用于磁盘写入;无则根据 image 编码写入)。
    ///   - key: 缓存键
    ///   - ttl: 条目级 TTL(若 nil 则使用 defaultTTL)
    ///   - costHintBytes: 内存 cost 估算(可 nil,由实现方估算)
    func store(image: UIImage,
               originalData: Data?,
               forKey key: String,
               ttl: TimeInterval?,
               costHintBytes: Int?) async

    // 命中检查
    func contains(_ key: String, in scope: CacheScope) async -> Bool

    // 失效与清理
    func remove(_ key: String, in scope: CacheScope) async
    func removeAll(in scope: CacheScope) async

    // 维护(可选增强)
    func trimToCapacityIfNeeded() async
}

// MARK: - 占位与预取

/// 占位图提供
public protocol ImagePlaceholderProviding: Sendable {
    func placeholder(for request: ImageRequest) -> UIImage?
}

/// 预取协议:用于快速滑动场景提前将磁盘项提升到内存,或提前下载写入磁盘。
public protocol ImagePrefetching: Sendable {
    /// 提前调度请求,使随后的正式加载快速命中(内存或磁盘)。
    func prefetch(_ requests: [ImageRequest])
    func cancelPrefetch(for requests: [ImageRequest])
}

// MARK: - 统一获取接口(组合)

/// 面向业务的统一入口:先查缓存(内存->磁盘->解码),未命中则由实现方决定是否走网络并回写缓存。
public protocol ImageProvider: Sendable, ImagePrefetching {
    var cache: ImageCache { get }
    var keyProvider: ImageKeyProviding { get }
    var placeholderProvider: ImagePlaceholderProviding? { get }

    /// 统一获取图片。实现方可按 request.cachePolicy 决定是否/如何走网络。
    func loadImage(with request: ImageRequest) async throws -> ImageLoadResult

    /// 失效处理
    func invalidate(keys: [String], scope: CacheScope) async
}

方法说明

  • ImageKeyProviding

    • makeKey(for:):
      • 用途:从请求生成稳定的缓存键
      • 参数:request(包含 URL、解码选项、尺寸等)
      • 返回:字符串 Key
      • 注意:Key 必须包含像素相关参数(例如下采样尺寸、scale),避免同 URL 不同显示需求错误复用
  • ImageCache

    • image(forKey:decoding:):
      • 用途:按 Key 读取图片。应先查内存;内存未命中则查磁盘并按 decoding 选项解码,再回填内存
      • 参数:key(缓存键)、decoding(解码选项,可为 nil)
      • 返回:UIImage 或 nil
    • store(image:originalData:forKey:ttl:costHintBytes:):
      • 用途:写入内存与磁盘;TTL 控制过期时间;容量控制在写入后触发裁剪
      • 参数:
        • image:已解码图片(写内存)
        • originalData:原始数据(优先写磁盘),缺省时可基于 image 编码
        • key:缓存键
        • ttl:条目级 TTL(nil 使用 defaultTTL)
        • costHintBytes:内存 cost 估算(如像素数×4)
    • contains(_:in:):
      • 用途:判断指定范围内是否存在有效缓存(未过期)
    • remove(_:in:), removeAll(in:):
      • 用途:删除指定键或全清空;scope 决定目标层级
    • trimToCapacityIfNeeded():
      • 用途:按容量上限在后台裁剪(LRU/按访问时间等)
  • ImagePlaceholderProviding

    • placeholder(for:):
      • 用途:提供 UI 立即可用的占位图
      • 注意:不应阻塞主线程(可返回预制图片)
  • ImagePrefetching

    • prefetch(_:), cancelPrefetch(for:):
      • 用途:在快速滑动中提前调度读取/下载,提升命中率
      • 注意:实现方应去重、限并发与可取消
  • ImageProvider

    • loadImage(with:):
      • 用途:统一入口:内存->磁盘->(可选)网络;网络成功后应回填缓存
      • 返回:ImageLoadResult(包含来源与最终 Key)
      • 错误:可抛出网络或解码错误
    • invalidate(keys:scope:):
      • 用途:对外暴露的失效操作,便于业务统一驱动清理

属性说明

  • ImageCache.memoryCapacityBytes, diskCapacityBytes
    • 缓存容量上限(字节),用于触发淘汰策略(建议 LRU)
  • ImageCache.defaultTTL
    • 默认过期时间(秒),条目级 TTL 优先级更高
  • ImageCache.keyProvider / ImageProvider.keyProvider
    • Key 生成器,确保组件间统一
  • ImageProvider.placeholderProvider
    • 占位图提供者,UI 层可在加载前同步获取

使用示例 以下示例包含:

  • DefaultKeyProvider:默认 Key 规则
  • DefaultImageCache:两级缓存实现(actor 并发安全,含 TTL 与容量裁剪)
  • DefaultImageProvider:统一获取接口(包含简单的网络下载)
  • 预取与占位示例

说明:示例为教学用途,聚焦核心路径与正确性,省略了部分健壮性(如错误域细分、磁盘 LRU 更精细的实现等),可直接编译运行于 iOS 15+。

import UIKit
import CryptoKit

// MARK: - Key Provider

public struct DefaultKeyProvider: ImageKeyProviding {
    public init() {}
    public func makeKey(for request: ImageRequest) -> String {
        if let override = request.overrideKey { return override }
        // 组合 URL + 解码关键参数,避免错误复用
        let base = [
            request.url.absoluteString,
            "dps=\(request.decodingOptions.downsampleToPixelSize?.debugDescription ?? "nil")",
            "sc=\(request.decodingOptions.scale)",
            "cm=\(request.decodingOptions.contentMode)"
        ].joined(separator: "|")
        // 哈希缩短长度
        let digest = Insecure.MD5.hash(data: Data(base.utf8))
        return digest.map { String(format: "%02hhx", $0) }.joined()
    }
}

// MARK: - 内存缓存(TTL + NSCache)

actor MemoryCacheBox {
    private let cache = NSCache<NSString, UIImage>()
    private var expiry: [String: Date] = [:]

    init(capacityBytes: Int) {
        cache.totalCostLimit = capacityBytes
    }

    func updateCapacity(_ bytes: Int) {
        cache.totalCostLimit = bytes
    }

    func image(forKey key: String, now: Date) -> UIImage? {
        if let exp = expiry[key], exp < now {
            cache.removeObject(forKey: key as NSString)
            expiry.removeValue(forKey: key)
            return nil
        }
        return cache.object(forKey: key as NSString)
    }

    func set(image: UIImage, forKey key: String, ttl: TimeInterval, cost: Int) {
        let expireAt = Date().addingTimeInterval(ttl)
        expiry[key] = expireAt
        cache.setObject(image, forKey: key as NSString, cost: cost)
    }

    func contains(_ key: String, now: Date) -> Bool {
        if let _ = image(forKey: key, now: now) { return true }
        return false
    }

    func remove(_ key: String) {
        cache.removeObject(forKey: key as NSString)
        expiry.removeValue(forKey: key)
    }

    func removeAll() {
        cache.removeAllObjects()
        expiry.removeAll()
    }
}

// MARK: - 磁盘缓存(TTL + 容量)

actor DiskCacheBox {
    private let fm = FileManager()
    private let directory: URL
    private var diskCapacityBytes: Int
    private let metaExt = "meta" // sidecar 元数据文件后缀

    struct Meta: Codable {
        let expireAt: Date
        let length: Int
        let lastAccessAt: Date
    }

    init(directoryName: String = "DefaultImageDiskCache", diskCapacityBytes: Int) throws {
        let base = try fm.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        directory = base.appendingPathComponent(directoryName, isDirectory: true)
        if !fm.fileExists(atPath: directory.path) {
            try fm.createDirectory(at: directory, withIntermediateDirectories: true)
        }
        self.diskCapacityBytes = diskCapacityBytes
    }

    func updateCapacity(_ bytes: Int) {
        self.diskCapacityBytes = bytes
    }

    private func dataURL(for key: String) -> URL { directory.appendingPathComponent(key) }
    private func metaURL(for key: String) -> URL { directory.appendingPathComponent(key).appendingPathExtension(metaExt) }

    func read(for key: String, now: Date) -> Data? {
        let dURL = dataURL(for: key)
        let mURL = metaURL(for: key)
        guard
            fm.fileExists(atPath: dURL.path),
            let mdata = try? Data(contentsOf: mURL),
            let meta = try? JSONDecoder().decode(Meta.self, from: mdata)
        else { return nil }

        if meta.expireAt < now {
            try? fm.removeItem(at: dURL)
            try? fm.removeItem(at: mURL)
            return nil
        }

        // 更新访问时间
        let newMeta = Meta(expireAt: meta.expireAt, length: meta.length, lastAccessAt: now)
        if let updated = try? JSONEncoder().encode(newMeta) {
            try? updated.write(to: mURL, options: .atomic)
        }
        return try? Data(contentsOf: dURL)
    }

    func write(data: Data, for key: String, ttl: TimeInterval) {
        let dURL = dataURL(for: key)
        let mURL = metaURL(for: key)
        do {
            try data.write(to: dURL, options: .atomic)
            let meta = Meta(expireAt: Date().addingTimeInterval(ttl),
                            length: data.count,
                            lastAccessAt: Date())
            let mdata = try JSONEncoder().encode(meta)
            try mdata.write(to: mURL, options: .atomic)
        } catch {
            // 失败则尝试清理并忽略
        }
        Task { await trimIfNeeded() }
    }

    func contains(_ key: String, now: Date) -> Bool {
        return read(for: key, now: now) != nil
    }

    func remove(_ key: String) {
        try? fm.removeItem(at: dataURL(for: key))
        try? fm.removeItem(at: metaURL(for: key))
    }

    func removeAll() {
        guard let items = try? fm.contentsOfDirectory(atPath: directory.path) else { return }
        for i in items {
            try? fm.removeItem(at: directory.appendingPathComponent(i))
        }
    }

    private func directorySizeAndMetas() -> (Int, [(String, Meta)]) {
        guard let items = try? fm.contentsOfDirectory(atPath: directory.path) else { return (0, []) }
        var total = 0
        var metas: [(String, Meta)] = []
        for name in items where !name.hasSuffix(".\(metaExt)") {
            let key = name
            let dURL = directory.appendingPathComponent(name)
            let mURL = metaURL(for: key)
            guard let mdata = try? Data(contentsOf: mURL),
                  let meta = try? JSONDecoder().decode(Meta.self, from: mdata)
            else { continue }
            total += meta.length
            metas.append((key, meta))
        }
        return (total, metas)
    }

    func trimIfNeeded() {
        var (total, metas) = directorySizeAndMetas()
        guard total > diskCapacityBytes else { return }
        // 按 lastAccessAt 升序淘汰
        metas.sort { $0.1.lastAccessAt < $1.1.lastAccessAt }
        for (key, meta) in metas {
            if total <= diskCapacityBytes { break }
            remove(key)
            total -= meta.length
        }
    }
}

// MARK: - 解码工具(下采样)

enum ImageDecode {
    static func decode(data: Data, options: ImageDecodingOptions?) -> UIImage? {
        guard let options, let target = options.downsampleToPixelSize else {
            return UIImage(data: data, scale: options?.scale ?? UIScreen.main.scale)
        }
        let cfData = data as CFData
        guard let src = CGImageSourceCreateWithData(cfData, nil) else {
            return UIImage(data: data, scale: options.scale)
        }
        let maxPixel: CGFloat = {
            switch options.contentMode {
            case .fit:
                return max(target.width, target.height)
            case .fill:
                return min(target.width, target.height)
            }
        }()
        let dict: [CFString: Any] = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,
            kCGImageSourceCreateThumbnailWithTransform: true,
            kCGImageSourceThumbnailMaxPixelSize: Int(maxPixel)
        ]
        guard let cg = CGImageSourceCreateThumbnailAtIndex(src, 0, dict as CFDictionary) else {
            return UIImage(data: data, scale: options.scale)
        }
        return UIImage(cgImage: cg, scale: options.scale, orientation: .up)
    }

    static func estimatedMemoryCostBytes(for image: UIImage) -> Int {
        guard let cg = image.cgImage else {
            let size = image.size
            let scale = image.scale
            return Int(size.width * scale * size.height * scale * 4)
        }
        return cg.bytesPerRow * cg.height
    }
}

// MARK: - 默认两级缓存实现

public final class DefaultImageCache: ImageCache {
    public var memoryCapacityBytes: Int {
        didSet { Task { await memory.updateCapacity(memoryCapacityBytes) } }
    }
    public var diskCapacityBytes: Int {
        didSet { Task { await disk.updateCapacity(diskCapacityBytes) } }
    }
    public var defaultTTL: TimeInterval
    public let keyProvider: ImageKeyProviding

    private let memory: MemoryCacheBox
    private let disk: DiskCacheBox

    public init(memoryCapacityBytes: Int = 64 * 1024 * 1024,
                diskCapacityBytes: Int = 512 * 1024 * 1024,
                defaultTTL: TimeInterval = 7 * 24 * 3600,
                keyProvider: ImageKeyProviding = DefaultKeyProvider()) {
        self.memoryCapacityBytes = memoryCapacityBytes
        self.diskCapacityBytes = diskCapacityBytes
        self.defaultTTL = defaultTTL
        self.keyProvider = keyProvider
        self.memory = MemoryCacheBox(capacityBytes: memoryCapacityBytes)
        self.disk = try! DiskCacheBox(diskCapacityBytes: diskCapacityBytes)
    }

    public func image(forKey key: String,
                      decoding: ImageDecodingOptions?) async -> UIImage? {
        let now = Date()
        if let img = await memory.image(forKey: key, now: now) { return img }
        // 磁盘命中则解码并回填内存
        if let data = await disk.read(for: key, now: now),
           let img = ImageDecode.decode(data: data, options: decoding) {
            let cost = ImageDecode.estimatedMemoryCostBytes(for: img)
            await memory.set(image: img, forKey: key, ttl: defaultTTL, cost: cost)
            return img
        }
        return nil
    }

    public func store(image: UIImage,
                      originalData: Data?,
                      forKey key: String,
                      ttl: TimeInterval?,
                      costHintBytes: Int?) async {
        let ttlValue = ttl ?? defaultTTL
        let cost = costHintBytes ?? ImageDecode.estimatedMemoryCostBytes(for: image)
        await memory.set(image: image, forKey: key, ttl: ttlValue, cost: cost)
        // 优先写入原始数据,否则回退编码 PNG(注意:PNG 编码成本较高)
        if let data = originalData {
            await disk.write(data: data, for: key, ttl: ttlValue)
        } else if let data = image.pngData() {
            await disk.write(data: data, for: key, ttl: ttlValue)
        }
    }

    public func contains(_ key: String, in scope: CacheScope) async -> Bool {
        let now = Date()
        switch scope {
        case .memory:
            return await memory.contains(key, now: now)
        case .disk:
            return await disk.contains(key, now: now)
        case .all:
            return await memory.contains(key, now: now) || await disk.contains(key, now: now)
        }
    }

    public func remove(_ key: String, in scope: CacheScope) async {
        switch scope {
        case .memory: await memory.remove(key)
        case .disk: await disk.remove(key)
        case .all:
            await memory.remove(key)
            await disk.remove(key)
        }
    }

    public func removeAll(in scope: CacheScope) async {
        switch scope {
        case .memory: await memory.removeAll()
        case .disk: await disk.removeAll()
        case .all:
            await memory.removeAll()
            await disk.removeAll()
        }
    }

    public func trimToCapacityIfNeeded() async {
        await disk.trimIfNeeded()
        // NSCache 自动按 totalCostLimit 淘汰,无需手动
    }
}

// MARK: - 占位提供者

public struct DefaultPlaceholderProvider: ImagePlaceholderProviding {
    public init() {}
    public func placeholder(for request: ImageRequest) -> UIImage? {
        // 简单的灰底占位,实现不应阻塞
        let size = CGSize(width: 16, height: 16)
        let format = UIGraphicsImageRendererFormat()
        format.scale = UIScreen.main.scale
        let img = UIGraphicsImageRenderer(size: size, format: format).image { ctx in
            UIColor(white: 0.95, alpha: 1).setFill()
            ctx.fill(CGRect(origin: .zero, size: size))
        }
        return img
    }
}

// MARK: - 默认 Provider(带网络)

public final class DefaultImageProvider: ImageProvider {
    public let cache: ImageCache
    public let keyProvider: ImageKeyProviding
    public let placeholderProvider: ImagePlaceholderProviding?

    private let session: URLSession
    private let decoder = ImageDecode.self
    private let defaultTTL: TimeInterval

    public init(cache: ImageCache = DefaultImageCache(),
                keyProvider: ImageKeyProviding = DefaultKeyProvider(),
                placeholderProvider: ImagePlaceholderProviding? = DefaultPlaceholderProvider(),
                session: URLSession = .shared,
                defaultTTL: TimeInterval = 7 * 24 * 3600) {
        self.cache = cache
        self.keyProvider = keyProvider
        self.placeholderProvider = placeholderProvider
        self.session = session
        self.defaultTTL = defaultTTL
    }

    public func loadImage(with request: ImageRequest) async throws -> ImageLoadResult {
        let key = keyProvider.makeKey(for: request)

        // 1) 按策略尝试缓存读取
        switch request.cachePolicy {
        case .reloadIgnoringCache:
            break
        case .reloadIgnoringMemory:
            if let img = await cache.image(forKey: key, decoding: request.decodingOptions) {
                // 已经从磁盘回填到内存,无需额外处理
                return ImageLoadResult(image: img, source: .disk, key: key)
            }
        default:
            if let img = await cache.image(forKey: key, decoding: request.decodingOptions) {
                return ImageLoadResult(image: img, source: .memory, key: key)
            }
        }

        // 2) 走网络
        var urlRequest = URLRequest(url: request.url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
        let (data, response) = try await session.data(for: urlRequest)
        guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
            throw URLError(.badServerResponse)
        }

        // 3) 解码(遵循 request.decodingOptions)
        guard let image = decoder.decode(data: data, options: request.decodingOptions) else {
            throw URLError(.cannotDecodeContentData)
        }

        // 4) 回写缓存
        await cache.store(image: image, originalData: data, forKey: key, ttl: defaultTTL, costHintBytes: nil)

        return ImageLoadResult(image: image, source: .network, key: key)
    }

    // MARK: ImagePrefetching

    private var prefetchTasks = [String: Task<Void, Never>]()
    public func prefetch(_ requests: [ImageRequest]) {
        for req in requests {
            let key = keyProvider.makeKey(for: req)
            if prefetchTasks[key] != nil { continue }
            // 若内存或磁盘已命中则跳过网络
            let task = Task.detached(priority: .utility) { [weak self] in
                guard let self else { return }
                if await self.cache.image(forKey: key, decoding: req.decodingOptions) != nil { return }
                var urlRequest = URLRequest(url: req.url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 15)
                if let (data, resp) = try? await self.session.data(for: urlRequest),
                   let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode),
                   let image = ImageDecode.decode(data: data, options: req.decodingOptions) {
                    await self.cache.store(image: image, originalData: data, forKey: key, ttl: self.defaultTTL, costHintBytes: nil)
                }
            }
            prefetchTasks[key] = task
        }
    }

    public func cancelPrefetch(for requests: [ImageRequest]) {
        for req in requests {
            let key = keyProvider.makeKey(for: req)
            prefetchTasks[key]?.cancel()
            prefetchTasks.removeValue(forKey: key)
        }
    }

    public func invalidate(keys: [String], scope: CacheScope) async {
        for k in keys { await cache.remove(k, in: scope) }
    }
}

// MARK: - 使用示例(UIKit)

final class FeedImageView: UIImageView {
    private var task: Task<Void, Never>?

    func setImage(with request: ImageRequest, provider: ImageProvider) {
        task?.cancel()
        self.image = provider.placeholderProvider?.placeholder(for: request)

        task = Task { [weak self] in
            guard let self else { return }
            do {
                let result = try await provider.loadImage(with: request)
                // UI 更新在主线程
                await MainActor.run { self.image = result.image }
            } catch {
                // 可设置错误占位
            }
        }
    }

    func cancelLoad() {
        task?.cancel()
        task = nil
    }
}

// MARK: - 结合 UICollectionView 预取(示意)

final class FeedViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDataSourcePrefetching {
    let provider = DefaultImageProvider()
    var items: [URL] = []

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { items.count }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! FeedCell
        let url = items[indexPath.item]
        let req = ImageRequest(url: url, decodingOptions: .init(downsampleToPixelSize: CGSize(width: 400, height: 400)))
        cell.imageView.setImage(with: req, provider: provider)
        return cell
    }

    // 预取:提前下载并落盘/入内存
    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
        let reqs = indexPaths.map { idx in
            ImageRequest(url: items[idx.item], decodingOptions: .init(downsampleToPixelSize: CGSize(width: 400, height: 400)))
        }
        provider.prefetch(reqs)
    }

    func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
        let reqs = indexPaths.map { idx in
            ImageRequest(url: items[idx.item], decodingOptions: .init(downsampleToPixelSize: CGSize(width: 400, height: 400)))
        }
        provider.cancelPrefetch(for: reqs)
    }
}

final class FeedCell: UICollectionViewCell {
    let imageView = FeedImageView()
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.addSubview(imageView)
        imageView.frame = contentView.bounds
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
    }
    required init?(coder: NSCoder) { fatalError() }
}

注意事项

  • 并发与线程安全

    • 协议实现需满足 Sendable 与并发安全。示例通过 actor 封装内存/磁盘状态;NSCache 本身是线程安全的,但 TTL 元数据需要 actor 保护
    • URLSession 回调为任意线程,UI 更新需切回主线程
  • Key 生成规则

    • Key 必须包含会影响像素内容的参数(URL、下采样尺寸、scale、contentMode 等),避免错误命中
    • 对于带鉴权或动态查询参数的 URL,如参数不影响像素内容,建议在生成 Key 前做归一化处理
  • TTL 与容量控制

    • TTL:示例对内存与磁盘均生效。磁盘以 sidecar 元数据存 expireAt/lastAccessAt 实现过期与近似 LRU
    • 容量:内存依赖 NSCache totalCostLimit;磁盘示例按 lastAccessAt 近似 LRU 裁剪。生产中可维护更精细的访问日志或使用文件访问时间
  • 解码与下采样

    • 解码在后台进行,避免主线程阻塞
    • 下采样目标尺寸建议使用“像素尺寸”,并结合屏幕 scale
    • 若需要渐进式/动图解码,可在 ImageDecodingOptions 扩展相应字段并扩展解码实现
  • 占位与预取

    • 占位图应为轻量级、同步可得的 UIImage
    • 预取应避免重复与过度并发,注意在列表快速滚动时取消不再可见项的预取
  • 网络与缓存一致性

    • 当请求的 HTTP 响应包含强缓存/协商缓存头时,可结合 URLCache 或自行将 TTL 绑定到响应头(如 Cache-Control max-age)来设定 per-entry TTL
    • 对错误响应应避免写入磁盘;对过大的图片建议直接下采样后写入,降低磁盘与内存压力
  • 内存告警

    • 可在收到 UIApplication.didReceiveMemoryWarningNotification 时调用 removeAll(in: .memory)
  • 单元测试建议

    • 覆盖点:Key 生成一致性、TTL 过期、容量裁剪、并发访问一致性、下采样正确性、网络失败回退、预取取消行为

通过以上协议与示例实现,可在 Feed 流和详情页中实现一致的图片获取与失效处理,降低重复下载,提升滑动场景的命中率与 UI 流畅度。

协议概述

该协议集用于统一约定“表单字段与整表校验流程”,覆盖以下能力:

  • 支持同步/异步规则验证(如本地规则与远程用户名占用校验)
  • 错误消息本地化(基于Localizable.strings)
  • 依赖字段联动(如“密码不包含用户名”)
  • 可组合校验器(顺序执行、失败收集策略)
  • 实时校验与提交时校验两种模式
  • 整表结果聚合与回调提示

适用场景:登录/注册页等表单组件,对用户名/密码/验证码进行实时校验,并在提交前汇总错误提示以提升转化。

注:示例基于 Swift 5.7+、iOS 15+(使用 async/await),不依赖第三方库,适合初级开发者集成与扩展。


协议定义

import Foundation

// MARK: - 错误本地化

/// 错误本地化协议:将 messageKey + arguments 转换为最终文案
public protocol ErrorLocalizer {
    /// - Parameters:
    ///   - key: 本地化 key(Localizable.strings)
    ///   - arguments: 文案占位符参数
    ///   - locale: 目标本地化区域(可忽略,系统会根据用户设置选择)
    func localizedString(for key: String, arguments: [String], locale: Locale) -> String
}

/// 默认基于主 Bundle 的本地化实现
public struct BundleErrorLocalizer: ErrorLocalizer {
    public let table: String?
    public let bundle: Bundle

    public init(table: String? = nil, bundle: Bundle = .main) {
        self.table = table
        self.bundle = bundle
    }

    public func localizedString(for key: String, arguments: [String], locale: Locale) -> String {
        let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "")
        return String(format: format, arguments: arguments)
    }
}

// MARK: - 校验模型

/// 单条校验问题
public struct ValidationIssue: Equatable, Sendable {
    /// 字段标识(推荐使用固定字符串,如 "username"、"password")
    public let fieldID: String
    /// 问题代码(便于统计/埋点)
    public let code: String
    /// 本地化 key(Localizable.strings)
    public let messageKey: String
    /// 文案占位符参数(字符串形式)
    public let arguments: [String]

    public init(fieldID: String, code: String, messageKey: String, arguments: [String] = []) {
        self.fieldID = fieldID
        self.code = code
        self.messageKey = messageKey
        self.arguments = arguments
    }

    /// 通过 Localizer 获取最终展示文案
    public func message(using localizer: ErrorLocalizer, locale: Locale) -> String {
        localizer.localizedString(for: messageKey, arguments: arguments, locale: locale)
    }
}

/// 校验上下文:提供跨字段访问与本地化环境
public struct ValidationContext {
    /// 全量字段值快照(值类型为 Any,规则内自行转换)
    public let values: [String: Any]
    /// 当前本地化区域
    public let locale: Locale
    /// 文案本地化器
    public let localizer: ErrorLocalizer

    public init(values: [String: Any], locale: Locale, localizer: ErrorLocalizer) {
        self.values = values
        this.locale = locale
        self.localizer = localizer
    }

    /// 按字段读取并转换为目标类型
    public func value<T>(for fieldID: String, as type: T.Type = T.self) -> T? {
        values[fieldID] as? T
    }
}

// MARK: - 单字段规则

/// 单字段规则(支持同步 + 异步)
public protocol FieldRule {
    /// 规则标识(便于调试与埋点)
    var id: String { get }

    /// 同步校验(常用于非网络类规则)
    /// - Returns: 存在问题返回单条问题,无问题返回 nil
    func validate(value: Any?, context: ValidationContext) -> ValidationIssue?

    /// 异步校验(常用于网络校验),默认无问题
    func validateAsync(value: Any?, context: ValidationContext) async -> ValidationIssue?
}

public extension FieldRule {
    func validateAsync(value: Any?, context: ValidationContext) async -> ValidationIssue? { nil }
}

// MARK: - 组合策略

/// 多规则组合策略
public enum CompositionStrategy {
    /// 遇到第一条失败即停止(性能优先/体验常用)
    case stopAtFirstFailure
    /// 收集全部失败(用于提交汇总)
    case collectAllFailures
}

// MARK: - 表单级别

/// 表单校验模式
public enum FormValidationMode {
    /// 实时校验(输入过程中触发)
    case realtime
    /// 提交时校验(点击提交按钮时一次性校验)
    case onSubmit
}

/// 表单校验协议:聚合字段规则、执行校验、提供结果回调
public protocol FormValidating: AnyObject {
    /// 模式:实时/提交时
    var mode: FormValidationMode { get set }
    /// 文案本地化器
    var localizer: ErrorLocalizer { get set }

    /// 获取某字段的规则集合
    func rules(for fieldID: String) -> [FieldRule]

    /// 实时校验问题变更回调(建议在主线程更新 UI)
    var onRealtimeIssuesChanged: (([String: [ValidationIssue]]) -> Void)? { get set }

    /// 提交时校验结果回调(聚合全部字段)
    var onSubmitIssues: (([String: [ValidationIssue]]) -> Void)? { get set }

    /// 校验单字段
    func validateField(_ fieldID: String,
                       values: [String: Any],
                       strategy: CompositionStrategy) async -> [ValidationIssue]

    /// 校验整表
    func validateAll(values: [String: Any],
                     strategy: CompositionStrategy) async -> [String: [ValidationIssue]]
}

方法说明

  • ErrorLocalizer.localizedString(for:arguments:locale:)

    • 用途:将 messageKey + arguments 转换为最终文案
    • 参数:
      • key:本地化键
      • arguments:占位符参数(按顺序替换)
      • locale:目标区域(通常可由系统语言自动处理)
    • 返回值:最终字符串
  • ValidationIssue.message(using:locale:)

    • 用途:基于 Localizer 获取最终展示文案
    • 参数:localizer、locale
    • 返回值:本地化后的文案
  • FieldRule.validate(value:context:)

    • 用途:执行单字段同步校验
    • 参数:
      • value:字段当前值(Any?,规则内自行转换成期望类型)
      • context:校验上下文,可获取其它字段值与本地化信息
    • 返回值:发现问题返回 ValidationIssue,否则返回 nil
  • FieldRule.validateAsync(value:context:)

    • 用途:执行单字段异步校验(如网络)
    • 返回值:发现问题返回 ValidationIssue,否则返回 nil
  • FormValidating.rules(for:)

    • 用途:为指定字段返回规则列表,按顺序执行
  • FormValidating.validateField(_:values:strategy:)

    • 用途:校验单个字段,支持组合策略
    • 返回值:该字段的所有问题列表(按策略可能为 0/1/多条)
  • FormValidating.validateAll(values:strategy:)

    • 用途:校验整表,返回字典:fieldID -> 问题列表
  • onRealtimeIssuesChanged

    • 用途:实时模式下,某字段问题变化后回调最新聚合结果
  • onSubmitIssues

    • 用途:提交按钮触发整表校验后的聚合回调

属性说明

  • FormValidating.mode: FormValidationMode

    • 实时或提交时校验模式
  • FormValidating.localizer: ErrorLocalizer

    • 错误文案本地化器
  • onRealtimeIssuesChanged: (([String: [ValidationIssue]]) -> Void)?

    • 实时校验结果变更回调;参数为当前所有字段的聚合问题字典
  • onSubmitIssues: (([String: [ValidationIssue]]) -> Void)?

    • 提交校验结果回调;参数为整表问题聚合字典

使用示例

以下示例演示「登录/注册」场景,对用户名/密码/验证码字段进行实时与提交时校验,含依赖字段与异步规则。字段标识采用字符串:

  • username
  • password
  • captcha
import UIKit

// MARK: - 具体规则实现

struct NonEmptyRule: FieldRule {
    let id = "non_empty"
    let fieldID: String
    let messageKey: String

    func validate(value: Any?, context: ValidationContext) -> ValidationIssue? {
        let text = value as? String
        if let t = text, !t.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            return nil
        }
        return ValidationIssue(fieldID: fieldID,
                               code: id,
                               messageKey: messageKey)
    }
}

struct MinLengthRule: FieldRule {
    let id = "min_length"
    let fieldID: String
    let min: Int
    let messageKey: String

    func validate(value: Any?, context: ValidationContext) -> ValidationIssue? {
        guard let text = value as? String, text.count >= min else {
            return ValidationIssue(fieldID: fieldID,
                                   code: id,
                                   messageKey: messageKey,
                                   arguments: ["\(min)"])
        }
        return nil
    }
}

struct RegexRule: FieldRule {
    let id = "regex"
    let fieldID: String
    let pattern: String
    let messageKey: String

    func validate(value: Any?, context: ValidationContext) -> ValidationIssue? {
        guard let text = value as? String else {
            return ValidationIssue(fieldID: fieldID,
                                   code: id,
                                   messageKey: messageKey)
        }
        let regex = try? NSRegularExpression(pattern: pattern)
        let range = NSRange(location: 0, length: (text as NSString).length)
        if let r = regex, r.firstMatch(in: text, options: [], range: range) != nil {
            return nil
        }
        return ValidationIssue(fieldID: fieldID,
                               code: id,
                               messageKey: messageKey)
    }
}

/// 依赖字段:密码不得包含用户名
struct NotContainFieldRule: FieldRule {
    let id = "not_contain_field"
    let fieldID: String
    let otherFieldID: String
    let messageKey: String

    func validate(value: Any?, context: ValidationContext) -> ValidationIssue? {
        guard
            let text = value as? String,
            let other: String = context.value(for: otherFieldID)
        else { return nil }

        if text.isEmpty || other.isEmpty { return nil }
        if text.lowercased().contains(other.lowercased()) {
            return ValidationIssue(fieldID: fieldID,
                                   code: id,
                                   messageKey: messageKey,
                                   arguments: [otherFieldID])
        }
        return nil
    }
}

/// 异步用户名占用检查(模拟网络)
struct RemoteUsernameAvailabilityRule: FieldRule {
    let id = "username_availability"
    let fieldID: String
    let messageKey: String

    func validate(value: Any?, context: ValidationContext) -> ValidationIssue? {
        // 本地同步校验先行(如非空)
        nil
    }

    func validateAsync(value: Any?, context: ValidationContext) async -> ValidationIssue? {
        guard let username = value as? String, !username.isEmpty else { return nil }

        // 模拟网络延时(生产环境换成 URLSession 调用)
        try? await Task.sleep(nanoseconds: 300_000_000)

        // 这里假设 "admin" 已被占用
        if username.lowercased() == "admin" {
            return ValidationIssue(fieldID: fieldID,
                                   code: id,
                                   messageKey: messageKey,
                                   arguments: [username])
        }
        return nil
    }
}

// MARK: - 简易表单校验器实现

@MainActor
public final class SimpleFormValidator: FormValidating {
    public var mode: FormValidationMode = .realtime
    public var localizer: ErrorLocalizer = BundleErrorLocalizer()

    public var onRealtimeIssuesChanged: (([String : [ValidationIssue]]) -> Void)?
    public var onSubmitIssues: (([String : [ValidationIssue]]) -> Void)?

    /// 字段 -> 规则数组
    private var rulesMap: [String: [FieldRule]] = [:]

    /// 内部状态:当前聚合的字段问题
    private var issuesMap: [String: [ValidationIssue]] = [:]

    public init() {}

    public func setRules(_ rules: [FieldRule], for fieldID: String) {
        rulesMap[fieldID] = rules
    }

    public func rules(for fieldID: String) -> [FieldRule] {
        rulesMap[fieldID] ?? []
    }

    public func validateField(_ fieldID: String,
                              values: [String : Any],
                              strategy: CompositionStrategy) async -> [ValidationIssue] {
        let context = ValidationContext(values: values, locale: .current, localizer: localizer)
        var fieldIssues: [ValidationIssue] = []
        let value = values[fieldID]

        // 1) 同步规则
        for rule in rules(for: fieldID) {
            if let issue = rule.validate(value: value, context: context) {
                fieldIssues.append(issue)
                if strategy == .stopAtFirstFailure { break }
            }
        }

        // 2) 异步规则(仅当同步未中断或策略为收集全部)
        if strategy == .collectAllFailures || fieldIssues.isEmpty {
            for rule in rules(for: fieldID) {
                if let issue = await rule.validateAsync(value: value, context: context) {
                    fieldIssues.append(issue)
                    if strategy == .stopAtFirstFailure { break }
                }
            }
        }

        // 3) 维护聚合状态并回调(实时模式)
        issuesMap[fieldID] = fieldIssues
        if mode == .realtime {
            onRealtimeIssuesChanged?(issuesMap)
        }
        return fieldIssues
    }

    public func validateAll(values: [String : Any],
                            strategy: CompositionStrategy) async -> [String : [ValidationIssue]] {
        var result: [String: [ValidationIssue]] = [:]
        for fieldID in rulesMap.keys {
            let issues = await validateField(fieldID, values: values, strategy: strategy)
            result[fieldID] = issues
        }
        // 提交模式回调
        onSubmitIssues?(result)
        return result
    }
}

// MARK: - 示例:登录/注册表单集成

final class AuthViewController: UIViewController {
    // 假设 storyboard 或代码创建
    @IBOutlet private weak var usernameField: UITextField!
    @IBOutlet private weak var passwordField: UITextField!
    @IBOutlet private weak var captchaField: UITextField!
    @IBOutlet private weak var submitButton: UIButton!

    private let validator = SimpleFormValidator()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupValidator()
        setupBindings()
    }

    private func setupValidator() {
        validator.mode = .realtime
        validator.localizer = BundleErrorLocalizer()

        // username 规则:非空、长度、格式、远程占用
        validator.setRules([
            NonEmptyRule(fieldID: "username", messageKey: "error.username.empty"),
            MinLengthRule(fieldID: "username", min: 3, messageKey: "error.username.min"),
            RegexRule(fieldID: "username",
                      pattern: "^[A-Za-z0-9_]{3,}$",
                      messageKey: "error.username.pattern"),
            RemoteUsernameAvailabilityRule(fieldID: "username", messageKey: "error.username.taken")
        ], for: "username")

        // password 规则:非空、长度、不得包含用户名
        validator.setRules([
            NonEmptyRule(fieldID: "password", messageKey: "error.password.empty"),
            MinLengthRule(fieldID: "password", min: 6, messageKey: "error.password.min"),
            NotContainFieldRule(fieldID: "password",
                                otherFieldID: "username",
                                messageKey: "error.password.contains_username")
        ], for: "password")

        // captcha 规则:非空
        validator.setRules([
            NonEmptyRule(fieldID: "captcha", messageKey: "error.captcha.empty")
        ], for: "captcha")

        // 回调:更新 UI 提示(此处仅打印,可替换为 label 或下划线高亮)
        validator.onRealtimeIssuesChanged = { [weak self] issuesMap in
            self?.applyIssues(issuesMap)
        }
        validator.onSubmitIssues = { [weak self] issuesMap in
            self?.applyIssues(issuesMap)
            // 若全部通过,执行提交
            if issuesMap.values.allSatisfy({ $0.isEmpty }) {
                self?.performSubmit()
            }
        }
    }

    private func setupBindings() {
        usernameField.addTarget(self, action: #selector(textChanged(_:)), for: .editingChanged)
        passwordField.addTarget(self, action: #selector(textChanged(_:)), for: .editingChanged)
        captchaField.addTarget(self, action: #selector(textChanged(_:)), for: .editingChanged)
    }

    @objc private func textChanged(_ sender: UITextField) {
        guard validator.mode == .realtime else { return }
        let values = currentValues()
        let fieldID: String
        switch sender {
        case usernameField: fieldID = "username"
        case passwordField: fieldID = "password"
        case captchaField:  fieldID = "captcha"
        default: return
        }
        Task { [weak self] in
            _ = await self?.validator.validateField(fieldID, values: values, strategy: .stopAtFirstFailure)
        }
    }

    @IBAction private func submitTapped(_ sender: UIButton) {
        Task { [weak self] in
            guard let self else { return }
            // 提交前整表校验,采用“收集全部失败”策略
            _ = await validator.validateAll(values: currentValues(), strategy: .collectAllFailures)
        }
    }

    private func currentValues() -> [String: Any] {
        [
            "username": usernameField.text ?? "",
            "password": passwordField.text ?? "",
            "captcha":  captchaField.text ?? ""
        ]
    }

    private func applyIssues(_ issuesMap: [String: [ValidationIssue]]) {
        // 示例:简单打印。实际中可更新每个文本框下的错误 Label
        let localizer = validator.localizer
        for (fieldID, issues) in issuesMap {
            let texts = issues.map { $0.message(using: localizer, locale: .current) }
            print("[\(fieldID)] issues: \(texts)")
        }
        // 可根据是否有错误启用/禁用提交按钮
        let hasError = issuesMap.values.contains { !$0.isEmpty }
        submitButton.isEnabled = !hasError
    }

    private func performSubmit() {
        // 执行登录/注册网络请求
        print("All validations passed. Proceed to submit.")
    }
}

示例 Localizable.strings(简体中文):

"error.username.empty" = "请输入用户名";
"error.username.min" = "用户名长度至少 %@ 位";
"error.username.pattern" = "用户名仅可包含字母、数字或下划线";
"error.username.taken" = "用户名“%@”已被占用";

"error.password.empty" = "请输入密码";
"error.password.min" = "密码长度至少 %@ 位";
"error.password.contains_username" = "密码不应包含用户名";

"error.captcha.empty" = "请输入验证码";

注意事项

  • 平台与语言
    • 示例使用 async/await,要求 Xcode 13.2+、Swift 5.5+,建议 iOS 15+ 运行环境。
  • 值类型与安全转换
    • 协议选择 [String: Any] 作为字段值容器,规则内部需将 Any? 转为预期类型(如 String)。请确保转换失败时返回合理错误或忽略。
  • 组合策略
    • 实时校验建议使用 .stopAtFirstFailure 提升体验与性能;
    • 提交时校验建议使用 .collectAllFailures 以一次性呈现全部问题。
  • 异步规则并发
    • 示例按顺序执行异步规则,便于控制依赖。若需并发可使用 TaskGroup,但要注意结果顺序与取消策略。
  • 本地化
    • 建议以稳定的 messageKey 管理文案,避免在代码中硬编码中文;
    • arguments 按顺序对应字符串格式化占位符(%@、%1$@ 等)。
  • 依赖字段联动
    • 通过 ValidationContext.values 读取其它字段值,注意空值与类型转换;
    • 当某字段变化后,如该字段被其它规则依赖,建议在实时模式下同时触发相关字段校验。
  • UI 线程
    • 回调中若更新 UI,请确保在主线程执行。示例的 SimpleFormValidator 标注了 @MainActor 以简化此问题。
  • 可扩展性
    • 可新增 FieldRule 实现(例如 EmailRule、ConfirmPasswordMatchRule、ServerCaptchaVerifyRule);
    • 可封装 AnyFieldRule(类型擦除)或引入类型安全的 Field 泛型包装,进一步增强可维护性。
  • 错误收敛
    • 生产中通常仅展示首条错误;可在 applyIssues 中自行裁剪展示策略。
  • 网络错误处理
    • 异步规则中应区分“网络失败”与“业务校验失败”,必要时返回不同 code 与文案 key,以便明确提示(如“网络异常,请重试”)。

以上协议与实现示例可直接用于登录/注册表单场景,并可按需扩展到其他表单页面。

示例详情

解决的问题

将“协议要实现什么”快速转化为一套结构完整、可直接落地的iOS协议文档。用户只需给出功能描述、应用场景与复杂度,即可自动生成含概述、定义、方法/属性说明、使用示例与注意事项的成体系内容,帮助团队从需求沟通、代码评审到交付归档全链路提效,把撰写时间从小时压缩到分钟,降低沟通成本,提升代码一致性与可维护性。

适用用户

iOS团队负责人

建立统一的协议文档标准;用一键生成覆盖各模块;在评审会上对照示例与注意事项快速达成一致,减少返工。

独立开发者

依据功能描述立即产出协议定义与示例;在紧张排期中节省写文档时间;交付客户或提交应用时资料更完整更规范。

初级工程师

通过清晰的方法说明与示例理解协议用法;按注意事项规避常见误用;在任务中独立完成组件对接与联调。

特征总结

根据需求描述,一键生成结构完备的Swift协议文档,概述到示例完整呈现。
内置专业写作范式,自动润色术语与段落,让团队快速读懂并无差沟通。
覆盖数据传递、控制器通信、网络与持久化等场景,常见协议文档轻松搞定。
为方法与属性自动补充分级注释、参数与返回说明,评审更高效,问题更可溯。
提供可直接运行的示例与实现要点,指导落地实践,缩短从设计到交付周期。
内置完整性与一致性校验,避免遗漏关键注意事项,降低联调与维护风险。
按技术复杂度智能调节篇幅与深度,新人易上手,资深也能快速对齐共识。
模板化与参数化支持,可复用团队标准,一键批量产出多模块统一文档。
遵循Swift与iOS最佳实践,内容准确及时,减少踩坑,降低技术债累积。
与研发流程紧密衔接,助力评审、交付与培训环节提效,明确责任边界。

如何使用购买的提示词模板

1. 直接在外部 Chat 应用中使用

将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。

2. 发布为 API 接口调用

把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。

3. 在 MCP Client 中配置使用

在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。

AI 提示词价格
¥20.00元
先用后买,用好了再付款,超安全!

您购买后可以获得什么

获得完整提示词模板
- 共 660 tokens
- 3 个可调节参数
{ 协议功能 } { 应用场景 } { 技术复杂度 }
获得社区贡献内容的使用权
- 精选社区优质案例,助您快速上手提示词
使用提示词兑换券,低至 ¥ 9.9
了解兑换券 →
限时半价

不要错过!

半价获取高级提示词-优惠即将到期

17
:
23
小时
:
59
分钟
:
59