热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
本提示词专为iOS开发者设计,能够根据协议的核心功能自动生成结构完整、技术准确的协议文档。它采用专业的技术写作风格,确保文档内容清晰、精确且逻辑严密,涵盖协议定义、方法说明、使用示例等关键要素,帮助开发者快速理解和应用iOS开发中的各种协议,提升开发效率和代码质量。
协议概述 该协议集统一定义基于 URLSession 的可重试网络请求策略与回退算法,适用于在弱网或服务短暂不可用时对 REST 客户端的 GET/HEAD 请求进行自动重试。核心能力包括:
协议定义
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:)
BackoffStrategy.nextDelay(for:category:context:)
BackoffStrategy.maxDelay
IdempotenceEvaluator.isIdempotent(_:)
CircuitBreaker.isOpen / canProceed / onSuccess / onFailure
RetryObserver 回调方法
NetworkRetryPolicy 属性与 shouldRetry
属性说明
NetworkRetryPolicy
BackoffStrategy
CircuitBreaker
使用示例 以下示例包含:
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)")
}
}
注意事项
平台与并发
幂等性与方法范围
超时与重试次数
退避与抖动
错误分类
熔断器
前后台与网络变化
指标与日志
线程安全
安全与合规
协议概述 该协议族用于规范 iOS 应用中图片缓存与获取流程,统一组件间的图片读取、写入、预取、失效与解码策略,目标是:
适用场景: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
ImageCache
ImagePlaceholderProviding
ImagePrefetching
ImageProvider
属性说明
使用示例 以下示例包含:
说明:示例为教学用途,聚焦核心路径与正确性,省略了部分健壮性(如错误域细分、磁盘 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() }
}
注意事项
并发与线程安全
Key 生成规则
TTL 与容量控制
解码与下采样
占位与预取
网络与缓存一致性
内存告警
单元测试建议
通过以上协议与示例实现,可在 Feed 流和详情页中实现一致的图片获取与失效处理,降低重复下载,提升滑动场景的命中率与 UI 流畅度。
该协议集用于统一约定“表单字段与整表校验流程”,覆盖以下能力:
适用场景:登录/注册页等表单组件,对用户名/密码/验证码进行实时校验,并在提交前汇总错误提示以提升转化。
注:示例基于 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:)
ValidationIssue.message(using:locale:)
FieldRule.validate(value:context:)
FieldRule.validateAsync(value:context:)
FormValidating.rules(for:)
FormValidating.validateField(_:values:strategy:)
FormValidating.validateAll(values:strategy:)
onRealtimeIssuesChanged
onSubmitIssues
FormValidating.mode: FormValidationMode
FormValidating.localizer: ErrorLocalizer
onRealtimeIssuesChanged: (([String: [ValidationIssue]]) -> Void)?
onSubmitIssues: (([String: [ValidationIssue]]) -> Void)?
以下示例演示「登录/注册」场景,对用户名/密码/验证码字段进行实时与提交时校验,含依赖字段与异步规则。字段标识采用字符串:
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" = "请输入验证码";
以上协议与实现示例可直接用于登录/注册表单场景,并可按需扩展到其他表单页面。
将“协议要实现什么”快速转化为一套结构完整、可直接落地的iOS协议文档。用户只需给出功能描述、应用场景与复杂度,即可自动生成含概述、定义、方法/属性说明、使用示例与注意事项的成体系内容,帮助团队从需求沟通、代码评审到交付归档全链路提效,把撰写时间从小时压缩到分钟,降低沟通成本,提升代码一致性与可维护性。
建立统一的协议文档标准;用一键生成覆盖各模块;在评审会上对照示例与注意事项快速达成一致,减少返工。
依据功能描述立即产出协议定义与示例;在紧张排期中节省写文档时间;交付客户或提交应用时资料更完整更规范。
通过清晰的方法说明与示例理解协议用法;按注意事项规避常见误用;在任务中独立完成组件对接与联调。
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
半价获取高级提示词-优惠即将到期