×
¥
查看详情
🔥 会员专享 文生文 代码生成

Swift代码片段生成专家

👁️ 104 次查看
📅 Dec 5, 2025
💡 核心价值: 本提示词专为iOS开发场景设计,能够根据用户描述的功能需求生成高质量的Swift代码片段。通过角色扮演和专业约束,确保输出的代码符合iOS开发最佳实践,包含必要的注释说明和错误处理机制。适用于各类iOS应用开发、功能实现和代码优化场景,帮助开发者快速获取精准的技术解决方案。

🎯 可自定义参数(3个)

功能描述
需要实现的iOS功能详细描述
开发场景
代码适用的具体开发场景
代码复杂度
所需代码的复杂程度

🎨 效果示例

功能概述

使用 SwiftUI 构建一个简洁的登录界面,采用 MVVM 结构与可注入的验证/网络层。支持:

  • 邮箱与密码实时校验(邮箱格式、密码至少 8 位)
  • 输入合法时启用“登录”按钮
  • 点击登录显示加载指示,并模拟 1 秒网络请求
  • 失败时弹出带“重试”的错误提示
  • 动态字体、深浅色适配与可访问性标签
  • iOS 15+,不依赖第三方库

代码实现

import SwiftUI

// MARK: - Validation Layer (可测试/可替换)
protocol AuthValidating {
    func isValidEmail(_ email: String) -> Bool
    func isValidPassword(_ password: String) -> Bool
}

struct DefaultAuthValidator: AuthValidating {
    // 简洁邮箱校验(满足大多数场景,便于单元测试)
    private let emailPredicate = NSPredicate(
        format: "SELF MATCHES[c] %@",
        "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
    )

    func isValidEmail(_ email: String) -> Bool {
        emailPredicate.evaluate(with: email)
    }

    func isValidPassword(_ password: String) -> Bool {
        password.count >= 8
    }
}

// MARK: - Service Layer (模拟网络)
enum AuthError: LocalizedError {
    case network
    case invalidCredentials

    var errorDescription: String? {
        switch self {
        case .network: return "网络异常,请稍后重试。"
        case .invalidCredentials: return "邮箱或密码错误。"
        }
    }
}

protocol AuthServicing {
    func login(email: String, password: String) async throws
}

struct MockAuthService: AuthServicing {
    enum Mode { case random, alwaysFail, alwaysSuccess }
    var mode: Mode = .random

    func login(email: String, password: String) async throws {
        // 模拟 1 秒网络耗时
        try await Task.sleep(nanoseconds: 1_000_000_000)

        // 可配置的结果,默认随机成功/失败
        let success: Bool
        switch mode {
        case .random:
            success = Bool.random()
        case .alwaysFail:
            success = false
        case .alwaysSuccess:
            success = true
        }

        if success {
            return
        } else {
            // 随机返回一种常见错误,便于演示
            throw Bool.random() ? AuthError.network : AuthError.invalidCredentials
        }
    }
}

// MARK: - ViewModel
@MainActor
final class LoginViewModel: ObservableObject {
    // Inputs
    @Published var email: String = ""
    @Published var password: String = ""

    // UI States
    @Published private(set) var isLoading: Bool = false
    @Published private(set) var errorMessage: String?
    @Published var showErrorAlert: Bool = false

    // Dependencies
    private let validator: AuthValidating
    private let authService: AuthServicing

    init(validator: AuthValidating = DefaultAuthValidator(),
         authService: AuthServicing = MockAuthService()) {
        self.validator = validator
        self.authService = authService
    }

    // Realtime validation
    var isEmailValid: Bool { validator.isValidEmail(email) }
    var isPasswordValid: Bool { validator.isValidPassword(password) }
    var isFormValid: Bool { isEmailValid && isPasswordValid }

    // Login action
    func login() async {
        guard isFormValid, !isLoading else { return }
        isLoading = true
        defer { isLoading = false }

        do {
            try await authService.login(email: email, password: password)
            // 登录成功,可在此发布成功事件或跳转
        } catch {
            errorMessage = (error as? LocalizedError)?.errorDescription ?? "未知错误。"
            showErrorAlert = true
        }
    }
}

// MARK: - View
struct LoginView: View {
    @StateObject private var viewModel = LoginViewModel()

    @FocusState private var focusedField: Field?
    private enum Field: Hashable { case email, password }

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("账户信息")) {
                    // 邮箱输入
                    TextField("邮箱", text: $viewModel.email)
                        .keyboardType(.emailAddress)
                        .textContentType(.emailAddress)
                        .textInputAutocapitalization(.never)
                        .disableAutocorrection(true)
                        .focused($focusedField, equals: .email)
                        .submitLabel(.next)
                        .accessibilityLabel("邮箱输入框")
                        .onSubmit {
                            focusedField = .password
                        }

                    if !viewModel.email.isEmpty && !viewModel.isEmailValid {
                        Text("请输入有效的邮箱地址")
                            .font(.footnote)
                            .foregroundColor(.red)
                            .accessibilityLabel("邮箱格式错误")
                    }

                    // 密码输入
                    SecureField("密码(至少 8 位)", text: $viewModel.password)
                        .textContentType(.password)
                        .focused($focusedField, equals: .password)
                        .submitLabel(.go)
                        .accessibilityLabel("密码输入框")
                        .accessibilityHint("密码至少 8 位")
                        .onSubmit {
                            if viewModel.isFormValid {
                                Task { await viewModel.login() }
                            }
                        }

                    if !viewModel.password.isEmpty && !viewModel.isPasswordValid {
                        Text("密码至少需要 8 位")
                            .font(.footnote)
                            .foregroundColor(.red)
                            .accessibilityLabel("密码长度不足")
                    }
                }

                Section {
                    Button(action: {
                        Task { await viewModel.login() }
                    }) {
                        HStack {
                            if viewModel.isLoading {
                                ProgressView()
                                    .progressViewStyle(.circular)
                                    .accessibilityLabel("正在登录")
                            }
                            Text("登录")
                                .font(.headline)
                        }
                        .frame(maxWidth: .infinity)
                    }
                    .buttonStyle(.borderedProminent)
                    .disabled(!viewModel.isFormValid || viewModel.isLoading)
                    .accessibilityLabel("登录按钮")
                    .accessibilityHint("当邮箱与密码合法时可点击")
                }
            }
            .navigationTitle("登录")
            .alert(viewModel.errorMessage ?? "登录失败", isPresented: $viewModel.showErrorAlert) {
                Button("重试") {
                    Task { await viewModel.login() }
                }
                Button("取消", role: .cancel) { }
            } message: {
                Text(viewModel.errorMessage ?? "请稍后重试。")
            }
        }
    }
}

// MARK: - Preview
struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LoginView()
                .preferredColorScheme(.light)
            LoginView()
                .preferredColorScheme(.dark)
                .environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge) // 动态字体预览
        }
    }
}

代码说明

  • 关键点1:MVVM 与可测试验证
    • 通过 AuthValidating 协议与 DefaultAuthValidator,将邮箱/密码校验逻辑独立,便于单元测试与替换。
    • ViewModel 仅持有协议依赖,既可注入真实服务,也可注入 Mock 服务,保持解耦。
  • 关键点2:异步登录与加载状态
    • 使用 async/await 与 Task.sleep 模拟 1 秒网络请求,期间设置 isLoading 控制 UI 状态(按钮禁用、显示 ProgressView)。
    • 登录失败后通过 showErrorAlert 与 errorMessage 触发 Alert,提供“重试”按钮再次调用登录逻辑。
  • 关键点3:实时校验与无障碍
    • 实时校验:isFormValid、isEmailValid、isPasswordValid 为计算属性,表单输入变化时自动刷新按钮可用性与错误提示。
    • 无障碍:为输入框、按钮、加载指示添加 accessibilityLabel;动态字体使用系统字体样式,适配深浅色主题。
    • 键盘行为:使用 FocusState 与 onSubmit 实现“下一项/提交”键流程,提升可用性。

适用场景

  • 基础登录页面的快速搭建(iOS 15+)
  • 需要可测试验证逻辑与可替换网络层的表单界面
  • 演示 SwiftUI 中表单实时校验、异步任务与加载/错误处理的最佳实践
  • 需要支持动态字体、无障碍和深浅色模式的登录/认证场景

功能概述

实现一个基于 URLSession 的 REST 客户端,用于请求 /articles 列表(GET),支持分页与关键字查询。代码包含:

  • Codable 解码
  • 超时控制与 3 次指数退避重试(含抖动)
  • ETag/If-None-Match 条件缓存与 304 处理
  • URLCache 持久化缓存,下线(离线)时回退读取缓存
  • 网络错误与状态码分类(401/403/5xx 等)
  • 线程安全设计(ETag 存储为 actor),易于单元测试(依赖注入)
  • 提供异步方法与简要使用示例
  • 无第三方库,遵循 iOS 最佳实践

代码实现

import Foundation

// MARK: - Models

/// 文章模型(示例字段,根据实际后端返回调整)
public struct Article: Codable, Hashable {
    public let id: String
    public let title: String
    public let author: String?
    public let summary: String?
    public let publishedAt: Date?
}

/// 分页响应通用模型(假设服务端返回此结构)
public struct PagedResponse<T: Codable>: Codable {
    public let items: [T]
    public let page: Int
    public let pageSize: Int
    public let total: Int?
}

// MARK: - Errors

public enum APIError: Error, CustomStringConvertible {
    case network(URLError)
    case timeout
    case cancelled
    case invalidResponse
    case decoding(Error)
    case unauthorized
    case forbidden
    case notModifiedNoCache
    case client(statusCode: Int, payloadSample: String?)
    case server(statusCode: Int, payloadSample: String?)
    case unknown(underlying: Error?)

    public var description: String {
        switch self {
        case .network(let e): return "Network error: \(e)"
        case .timeout: return "Request timed out"
        case .cancelled: return "Request cancelled"
        case .invalidResponse: return "Invalid HTTP response"
        case .decoding(let e): return "Decoding error: \(e)"
        case .unauthorized: return "401 Unauthorized"
        case .forbidden: return "403 Forbidden"
        case .notModifiedNoCache: return "304 Not Modified but no cached response available"
        case .client(let code, let sample): return "Client error \(code): \(sample ?? "<no body>")"
        case .server(let code, let sample): return "Server error \(code): \(sample ?? "<no body>")"
        case .unknown(let u): return "Unknown error: \(u.map { "\($0)" } ?? "<nil>")"
        }
    }
}

// MARK: - Testability Protocols

public protocol URLSessioning {
    func data(for request: URLRequest) async throws -> (Data, URLResponse)
}

extension URLSession: URLSessioning {}

public protocol URLCaching {
    func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest)
    func cachedResponse(for request: URLRequest) -> CachedURLResponse?
}

extension URLCache: URLCaching {}

// MARK: - ETag Store (Thread-safe)

public protocol ETagStoring {
    func etag(for key: String) async -> String?
    func setEtag(_ etag: String?, for key: String) async
}

/// 使用 actor 保证线程安全;持久化到 UserDefaults 便于 App 重启后继续使用
public actor DefaultETagStore: ETagStoring {
    private let defaults: UserDefaults
    private let namespace = "com.example.etagstore"
    public init(defaults: UserDefaults = .standard) { self.defaults = defaults }

    public func etag(for key: String) async -> String? {
        defaults.string(forKey: namespaced(key))
    }

    public func setEtag(_ etag: String?, for key: String) async {
        let nsKey = namespaced(key)
        if let etag {
            defaults.set(etag, forKey: nsKey)
        } else {
            defaults.removeObject(forKey: nsKey)
        }
    }

    private func namespaced(_ key: String) -> String { "\(namespace)::\(key)" }
}

// MARK: - Retry Policy

public struct RetryPolicy {
    public let maxRetries: Int
    public let baseDelay: TimeInterval // seconds
    public let maxDelay: TimeInterval // cap

    public init(maxRetries: Int = 3, baseDelay: TimeInterval = 0.4, maxDelay: TimeInterval = 5.0) {
        self.maxRetries = maxRetries
        self.baseDelay = baseDelay
        self.maxDelay = maxDelay
    }

    /// 指数退避+抖动
    public func backoffDelay(for attempt: Int) -> TimeInterval {
        guard attempt > 0 else { return 0 }
        let exp = min(maxDelay, baseDelay * pow(2, Double(attempt - 1)))
        // 抖动 ±20%
        let jitterFactor = Double.random(in: 0.8...1.2)
        return min(maxDelay, exp * jitterFactor)
    }
}

// MARK: - Client Config

public struct ArticlesAPIClientConfig {
    public let baseURL: URL
    public let timeout: TimeInterval
    public let session: URLSessioning
    public let cache: URLCaching
    public let decoder: JSONDecoder
    public let retryPolicy: RetryPolicy
    public let etagStore: ETagStoring

    public init(
        baseURL: URL,
        timeout: TimeInterval = 15,
        session: URLSessioning = URLSession.shared,
        cache: URLCaching = URLCache.shared,
        decoder: JSONDecoder = ArticlesAPIClientConfig.makeDefaultDecoder(),
        retryPolicy: RetryPolicy = RetryPolicy(),
        etagStore: ETagStoring = DefaultETagStore()
    ) {
        self.baseURL = baseURL
        self.timeout = timeout
        self.session = session
        self.cache = cache
        self.decoder = decoder
        self.retryPolicy = retryPolicy
        self.etagStore = etagStore
    }

    private static func makeDefaultDecoder() -> JSONDecoder {
        let d = JSONDecoder()
        d.keyDecodingStrategy = .convertFromSnakeCase
        if #available(iOS 15, *) {
            d.dateDecodingStrategy = .iso8601
        }
        return d
    }
}

// MARK: - Response Wrapper

public struct FetchResult<T: Decodable> {
    public let value: T
    public let isFromCache: Bool
    public let etag: String?
    public let response: HTTPURLResponse?
}

// MARK: - API Client

public final class ArticlesAPIClient {
    private let cfg: ArticlesAPIClientConfig

    public init(config: ArticlesAPIClientConfig) {
        self.cfg = config
    }

    /// 获取文章列表(分页+关键字搜索)
    /// - Parameters:
    ///   - page: 从 1 开始
    ///   - pageSize: 每页条数
    ///   - keyword: 关键字(可选)
    /// - Returns: 解码后的分页响应,带缓存标记与 ETag
    public func fetchArticles(page: Int, pageSize: Int, keyword: String?) async throws -> FetchResult<PagedResponse<Article>> {
        let path = "/articles"
        var items: [URLQueryItem] = [
            URLQueryItem(name: "page", value: String(max(page, 1))),
            URLQueryItem(name: "pageSize", value: String(max(pageSize, 1)))
        ]
        if let kw = keyword, !kw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            items.append(URLQueryItem(name: "keyword", value: kw))
        }
        let request = try makeGETRequest(path: path, queryItems: items, timeout: cfg.timeout)
        return try await execute(request: request, as: PagedResponse<Article>.self)
    }

    // MARK: - Core execution

    private func execute<T: Decodable>(request baseRequest: URLRequest, as type: T.Type) async throws -> FetchResult<T> {
        // 构建带 If-None-Match 的请求
        let key = cacheKey(for: baseRequest)
        var request = baseRequest
        if let etag = await cfg.etagStore.etag(for: key) {
            request.setValue(etag, forHTTPHeaderField: "If-None-Match")
        }

        let policy = cfg.retryPolicy
        var lastError: Error?

        for attempt in 0...(policy.maxRetries) {
            try Task.checkCancellation()
            if attempt > 0 {
                let delay = policy.backoffDelay(for: attempt)
                if delay > 0 { try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) }
            }

            do {
                let (data, urlResponse) = try await cfg.session.data(for: request)
                guard let http = urlResponse as? HTTPURLResponse else {
                    throw APIError.invalidResponse
                }

                switch http.statusCode {
                case 200...299:
                    // 读取并保存 ETag
                    let etag = http.value(forHTTPHeaderField: "ETag")
                    await cfg.etagStore.setEtag(etag, for: key)

                    // 缓存下行数据(即使服务端未提供 Cache-Control,也进行持久化)
                    let cached = CachedURLResponse(response: http, data: data, userInfo: etag.map { ["ETag": $0] }, storagePolicy: .allowed)
                    cfg.cache.storeCachedResponse(cached, for: request)

                    let model = try decode(type, from: data)
                    return FetchResult(value: model, isFromCache: false, etag: etag, response: http)

                case 304:
                    // 未修改 => 读取本地缓存
                    if let cached = cfg.cache.cachedResponse(for: request) {
                        let model = try decode(type, from: cached.data)
                        let etag = (cached.userInfo?["ETag"] as? String) ?? (await cfg.etagStore.etag(for: key))
                        return FetchResult(value: model, isFromCache: true, etag: etag, response: http)
                    } else {
                        throw APIError.notModifiedNoCache
                    }

                case 401:
                    throw APIError.unauthorized
                case 403:
                    throw APIError.forbidden
                case 500...599:
                    // 可重试的服务端错误
                    if attempt < policy.maxRetries {
                        continue
                    } else {
                        let sample = sampleString(from: data)
                        throw APIError.server(statusCode: http.statusCode, payloadSample: sample)
                    }

                default:
                    // 4xx 其他错误不重试
                    let sample = sampleString(from: data)
                    if (400...499).contains(http.statusCode) {
                        throw APIError.client(statusCode: http.statusCode, payloadSample: sample)
                    } else {
                        throw APIError.unknown(underlying: nil)
                    }
                }

            } catch {
                lastError = error
                // 分类错误并决定是否重试或读缓存
                if let urlErr = error as? URLError {
                    if urlErr.code == .timedOut {
                        if attempt < policy.maxRetries {
                            continue
                        } else {
                            // 超时最终失败前尝试读缓存
                            if let cached = cfg.cache.cachedResponse(for: request) {
                                let model = try decode(T.self, from: cached.data)
                                let etag = (cached.userInfo?["ETag"] as? String) ?? (await cfg.etagStore.etag(for: key))
                                return FetchResult(value: model, isFromCache: true, etag: etag, response: cached.response as? HTTPURLResponse)
                            }
                            throw APIError.timeout
                        }
                    }

                    // 可重试网络错误
                    let retryable: Set<URLError.Code> = [.networkConnectionLost, .notConnectedToInternet, .cannotFindHost, .cannotConnectToHost, .dnsLookupFailed, .resourceUnavailable]
                    if retryable.contains(urlErr.code), attempt < policy.maxRetries {
                        continue
                    }

                    // 离线/失败时回退缓存
                    if [.notConnectedToInternet, .networkConnectionLost, .cannotConnectToHost].contains(urlErr.code) {
                        if let cached = cfg.cache.cachedResponse(for: request) {
                            let model = try decode(T.self, from: cached.data)
                            let etag = (cached.userInfo?["ETag"] as? String) ?? (await cfg.etagStore.etag(for: key))
                            return FetchResult(value: model, isFromCache: true, etag: etag, response: cached.response as? HTTPURLResponse)
                        }
                    }

                    // 不可重试或无缓存
                    if urlErr.code == .cancelled { throw APIError.cancelled }
                    if urlErr.code == .timedOut { throw APIError.timeout }
                    throw APIError.network(urlErr)
                } else if error is CancellationError {
                    throw APIError.cancelled
                } else if attempt < policy.maxRetries {
                    continue
                } else {
                    // 最终失败前尝试读缓存
                    if let cached = cfg.cache.cachedResponse(for: request) {
                        let model = try decode(T.self, from: cached.data)
                        let etag = (cached.userInfo?["ETag"] as? String) ?? (await cfg.etagStore.etag(for: key))
                        return FetchResult(value: model, isFromCache: true, etag: etag, response: cached.response as? HTTPURLResponse)
                    }
                    throw APIError.unknown(underlying: error)
                }
            }
        }

        // 理论上不会走到这里
        throw APIError.unknown(underlying: lastError)
    }

    // MARK: - Helpers

    private func makeGETRequest(path: String, queryItems: [URLQueryItem], timeout: TimeInterval) throws -> URLRequest {
        var components = URLComponents(url: cfg.baseURL, resolvingAgainstBaseURL: false)
        // 确保 path 拼接正确
        let normalizedPath: String
        if path.hasPrefix("/") {
            normalizedPath = path
        } else {
            normalizedPath = "/" + path
        }
        components?.path = (components?.path ?? "") + normalizedPath
        components?.queryItems = queryItems.isEmpty ? nil : queryItems

        guard let url = components?.url else {
            throw APIError.unknown(underlying: URLError(.badURL))
        }

        var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: timeout)
        request.httpMethod = "GET"
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        return request
    }

    private func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
        do {
            return try cfg.decoder.decode(T.self, from: data)
        } catch {
            throw APIError.decoding(error)
        }
    }

    private func cacheKey(for request: URLRequest) -> String {
        // 使用完整 URL(含查询)作为 key
        request.url?.absoluteString ?? UUID().uuidString
    }

    private func sampleString(from data: Data, maxLen: Int = 200) -> String? {
        guard !data.isEmpty else { return nil }
        let s = String(data: data, encoding: .utf8) ?? "<non-utf8>"
        if s.count > maxLen {
            let idx = s.index(s.startIndex, offsetBy: maxLen)
            return String(s[..<idx]) + "…"
        }
        return s
    }
}

// MARK: - Usage Example (示例用法)

/// 使用示例(在 async 环境中调用)
///
/// let config = ArticlesAPIClientConfig(
///     baseURL: URL(string: "https://api.example.com")!,
///     timeout: 15
/// )
/// let client = ArticlesAPIClient(config: config)
///
/// Task {
///     do {
///         let result = try await client.fetchArticles(page: 1, pageSize: 20, keyword: "swift")
///         print("fromCache:", result.isFromCache, "etag:", result.etag ?? "nil")
///         print("items:", result.value.items.count)
///     } catch {
///         print("fetch failed:", error)
///     }
/// }

代码说明

  • 关键点1:网络层抽象与可测试性

    • 通过 URLSessioning、URLCaching、ETagStoring 协议进行依赖注入,便于单元测试中替换为 Mock。
    • ETag 存储使用 actor(DefaultETagStore),保证线程安全。
  • 关键点2:缓存策略与 ETag

    • 请求前读取已存 ETag,放入 If-None-Match;返回 304 时,从 URLCache 读取并解码。
    • 对 2xx 成功响应,手动写入 URLCache(即使服务端未提供缓存头),同时更新 ETag。
    • 离线或可识别的网络错误时优先回退读取缓存,提升可用性。
  • 关键点3:重试与错误分类

    • 指数退避(含抖动)重试最多 3 次,针对 5xx 和部分 URLError(如网络断开、DNS 失败、超时等)进行重试。
    • 对常见状态码分类处理:401(Unauthorized)、403(Forbidden)、5xx(Server),其余 4xx 归类为客户端错误并返回部分响应示例。
    • 对超时、取消、解码等错误有明确的错误类型,便于上层区分 UI/逻辑处理。
  • 关键点4:超时与线程安全

    • 每个请求使用 URLRequest 的 timeoutInterval 控制超时。
    • 客户端为无状态服务类;共享资源(ETag)通过 actor 管控;URLSession/URLCache 为线程安全。
  • 关键点5:解码与模型

    • JSONDecoder 使用 convertFromSnakeCase 和 ISO8601 日期策略,适配常见后端风格。
    • 示例模型 Article 与 PagedResponse 可按实际返回进行调整。

适用场景

  • 基于 REST 的列表拉取与搜索
  • 需要稳定性与离线可用性的内容型应用(新闻、博客、知识库等)
  • 需要 ETag 条件请求、URLCache 持久化与重试策略的生产级网络层
  • 需对错误类型精细化处理(鉴权过期、权限不足、服务异常等)
  • 需要线程安全与易于单元测试的网络抽象层

功能概述

实现一个使用 Swift 并发(TaskGroup)进行图片批量下载的组件:

  • 支持输入 URL 数组并发下载,最大并发限制为 4
  • 每个任务设置 5 秒超时(URLSession 请求超时)
  • 下载完成后按目标尺寸进行缩放并存入内存缓存(NSCache)
  • 在主线程回调进度与完成,用于更新 UI
  • 支持任务取消,单个失败不影响其他任务
  • 避免数据竞争(使用 actor 隔离缓存),包含必要注释

代码实现

import UIKit

/// 每个下载项的结果(成功时 image 不为 nil,失败时 error 不为 nil)
struct ImageDownloadItem {
    let url: URL
    let image: UIImage?
    let error: Error?
}

/// 线程安全的图片缓存(使用 actor 隔离访问,避免数据竞争)
actor ImageCache {
    private let cache = NSCache<NSURL, UIImage>()
    
    init(totalCostLimitInBytes: Int = 64 * 1024 * 1024, countLimit: Int = 200) {
        cache.totalCostLimit = totalCostLimitInBytes
        cache.countLimit = countLimit
    }
    
    func image(for url: URL) -> UIImage? {
        cache.object(forKey: url as NSURL)
    }
    
    func set(_ image: UIImage, for url: URL) {
        // 使用像素成本优化缓存淘汰策略
        let cost = Int(image.size.width * image.size.height * (image.cgImage?.bitsPerPixel ?? 32) / 8)
        cache.setObject(image, forKey: url as NSURL, cost: cost)
    }
    
    func removeAll() {
        cache.removeAllObjects()
    }
}

/// 批量图片下载器,使用 TaskGroup 并发,并限制最大并发数
final class ImageBatchDownloader {
    private let cache = ImageCache()
    private var currentTask: Task<Void, Never>?   // 用于取消当前批次任务
    private let session: URLSession
    
    init() {
        let config = URLSessionConfiguration.ephemeral
        config.timeoutIntervalForRequest = 5         // 单个请求 5 秒超时
        config.requestCachePolicy = .reloadIgnoringLocalCacheData
        config.waitsForConnectivity = false
        self.session = URLSession(configuration: config)
    }
    
    /// 启动批量下载(带进度与完成回调,均在主线程回调)
    func startBatch(
        urls: [URL],
        targetSize: CGSize,
        maxConcurrent: Int = 4,
        onProgress: ((ImageDownloadItem) -> Void)? = nil,
        onCompletion: (([ImageDownloadItem]) -> Void)? = nil
    ) {
        // 取消上一次批次(如果存在)
        currentTask?.cancel()
        
        // 启动新的批次任务
        currentTask = Task { [weak self] in
            guard let self = self else { return }
            let results = await self.downloadBatch(urls: urls, targetSize: targetSize, maxConcurrent: maxConcurrent, onProgress: onProgress)
            // 若已取消,则不回调完成
            if Task.isCancelled { return }
            if let onCompletion = onCompletion {
                await MainActor.run {
                    onCompletion(results)
                }
            }
        }
    }
    
    /// 取消当前批次
    func cancel() {
        currentTask?.cancel()
        currentTask = nil
    }
    
    /// 直接以 async 返回结果(无回调,用于纯数据层调用)
    func download(urls: [URL], targetSize: CGSize, maxConcurrent: Int = 4) async -> [ImageDownloadItem] {
        await downloadBatch(urls: urls, targetSize: targetSize, maxConcurrent: maxConcurrent, onProgress: nil)
    }
    
    // MARK: - 私有方法
    
    /// 执行批量下载并返回结果数组。进度将逐项回调(主线程)。
    private func downloadBatch(
        urls: [URL],
        targetSize: CGSize,
        maxConcurrent: Int,
        onProgress: ((ImageDownloadItem) -> Void)?
    ) async -> [ImageDownloadItem] {
        // 去重,避免重复请求
        let uniqueURLs = Array(Set(urls))
        var iterator = uniqueURLs.makeIterator()
        var results: [ImageDownloadItem] = []
        
        // 使用 TaskGroup 控制并发:先添加最多 maxConcurrent 个任务,之后按完成继续添加
        await withTaskGroup(of: ImageDownloadItem?.self) { group in
            let initial = min(maxConcurrent, uniqueURLs.count)
            for _ in 0..<initial {
                if let url = iterator.next() {
                    group.addTask { [weak self] in
                        guard let self = self else { return nil }
                        return await self.downloadAndProcess(url: url, targetSize: targetSize)
                    }
                }
            }
            
            while let item = await group.next() {
                if Task.isCancelled { group.cancelAll(); break }
                if let item = item {
                    results.append(item)
                    if let onProgress = onProgress {
                        await MainActor.run {
                            onProgress(item)
                        }
                    }
                }
                // 持续补充任务直到所有 URL 被消费
                if let nextURL = iterator.next() {
                    group.addTask { [weak self] in
                        guard let self = self else { return nil }
                        return await self.downloadAndProcess(url: nextURL, targetSize: targetSize)
                    }
                }
            }
        }
        
        return results
    }
    
    /// 单个 URL 的下载与缩放处理(失败不抛错,返回包含 error 的结果)
    private func downloadAndProcess(url: URL, targetSize: CGSize) async -> ImageDownloadItem? {
        if Task.isCancelled { return nil }
        
        // 命中缓存直接返回
        if let cached = await cache.image(for: url) {
            return ImageDownloadItem(url: url, image: cached, error: nil)
        }
        
        do {
            // 下载数据(5 秒超时由 session 配置保证)
            let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 5)
            let (data, response) = try await session.data(for: request)
            
            guard !Task.isCancelled else { return nil }
            
            // 校验 HTTP 响应码
            if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
                throw URLError(.badServerResponse)
            }
            
            // 数据转图片
            guard let original = UIImage(data: data) else {
                throw URLError(.cannotDecodeContentData)
            }
            
            // 按目标尺寸缩放(等比缩放,尽量适配目标尺寸)
            let scaled = scaleImage(original, toFit: targetSize)
            
            // 写入缓存
            await cache.set(scaled, for: url)
            
            return ImageDownloadItem(url: url, image: scaled, error: nil)
        } catch {
            // 单个失败不影响其他任务
            return ImageDownloadItem(url: url, image: nil, error: error)
        }
    }
}

/// 使用 UIGraphicsImageRenderer 安全缩放图片,按目标尺寸等比适配
private func scaleImage(_ image: UIImage, toFit targetSize: CGSize) -> UIImage {
    guard targetSize.width > 0, targetSize.height > 0 else { return image }
    let aspectWidth = targetSize.width / image.size.width
    let aspectHeight = targetSize.height / image.size.height
    let ratio = min(aspectWidth, aspectHeight)
    let newSize = CGSize(width: max(1, image.size.width * ratio), height: max(1, image.size.height * ratio))
    
    // 使用设备屏幕 scale 以获得更清晰的渲染
    let format = UIGraphicsImageRendererFormat.default()
    format.scale = UIScreen.main.scale
    format.opaque = false
    
    let renderer = UIGraphicsImageRenderer(size: newSize, format: format)
    return renderer.image { _ in
        image.draw(in: CGRect(origin: .zero, size: newSize))
    }
}

/// MARK: - 示例(SwiftUI)
import SwiftUI

struct BatchDownloadDemoView: View {
    @State private var images: [URL: UIImage] = [:]
    @State private var isLoading = false
    private let downloader = ImageBatchDownloader()
    
    // 示例 URL 列表(请替换为真实图片地址)
    let urls: [URL] = [
        URL(string: "https://picsum.photos/300/300?1")!,
        URL(string: "https://picsum.photos/300/300?2")!,
        URL(string: "https://picsum.photos/300/300?3")!,
        URL(string: "https://picsum.photos/300/300?4")!,
        URL(string: "https://picsum.photos/300/300?5")!,
        URL(string: "https://picsum.photos/300/300?6")!,
    ]
    
    let targetSize = CGSize(width: 120, height: 120)
    
    var body: some View {
        VStack {
            ScrollView {
                LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
                    ForEach(urls, id: \.self) { url in
                        if let img = images[url] {
                            Image(uiImage: img)
                                .resizable()
                                .scaledToFill()
                                .frame(width: targetSize.width, height: targetSize.height)
                                .clipped()
                                .cornerRadius(8)
                        } else {
                            ZStack {
                                Rectangle().fill(Color.gray.opacity(0.2))
                                    .frame(width: targetSize.width, height: targetSize.height)
                                    .cornerRadius(8)
                                ProgressView()
                            }
                        }
                    }
                }
                .padding()
            }
            
            HStack {
                Button(isLoading ? "取消" : "开始下载") {
                    if isLoading {
                        downloader.cancel()
                        isLoading = false
                    } else {
                        images.removeAll()
                        isLoading = true
                        downloader.startBatch(
                            urls: urls,
                            targetSize: targetSize,
                            maxConcurrent: 4,
                            onProgress: { item in
                                // 已在主线程回调,安全更新 UI 状态
                                if let img = item.image {
                                    images[item.url] = img
                                }
                            },
                            onCompletion: { _ in
                                isLoading = false
                            }
                        )
                    }
                }
                .buttonStyle(.borderedProminent)
            }
            .padding()
        }
        .onDisappear {
            downloader.cancel()
            isLoading = false
        }
    }
}

/// MARK: - 示例(UIKit)
final class BatchDownloadViewController: UIViewController, UICollectionViewDataSource {
    private let downloader = ImageBatchDownloader()
    private var images: [URL: UIImage] = [:]
    private var urls: [URL] = []
    
    private lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 120, height: 120)
        layout.minimumInteritemSpacing = 8
        layout.minimumLineSpacing = 8
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.dataSource = self
        cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        cv.backgroundColor = .systemBackground
        return cv
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(collectionView)
        collectionView.frame = view.bounds
        
        urls = [
            URL(string: "https://picsum.photos/300/300?10")!,
            URL(string: "https://picsum.photos/300/300?11")!,
            URL(string: "https://picsum.photos/300/300?12")!,
            URL(string: "https://picsum.photos/300/300?13")!,
        ]
        
        downloader.startBatch(
            urls: urls,
            targetSize: CGSize(width: 120, height: 120),
            maxConcurrent: 4,
            onProgress: { [weak self] item in
                guard let self = self else { return }
                if let image = item.image {
                    self.images[item.url] = image
                    self.collectionView.reloadData()
                }
            },
            onCompletion: { [weak self] _ in
                self?.collectionView.reloadData()
            }
        )
    }
    
    deinit {
        downloader.cancel()
    }
    
    // MARK: UICollectionViewDataSource
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        urls.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let url = urls[indexPath.item]
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        let imageViewTag = 1001
        let imageView: UIImageView
        if let existing = cell.contentView.viewWithTag(imageViewTag) as? UIImageView {
            imageView = existing
        } else {
            imageView = UIImageView(frame: cell.contentView.bounds)
            imageView.contentMode = .scaleAspectFill
            imageView.clipsToBounds = true
            imageView.tag = imageViewTag
            cell.contentView.addSubview(imageView)
        }
        imageView.frame = cell.contentView.bounds
        imageView.image = images[url] ?? nil
        cell.contentView.backgroundColor = .secondarySystemBackground
        return cell
    }
}

代码说明

  • 关键点1:并发与限流
    • 使用 TaskGroup 实现批量并发;先创建最多 4 个子任务,随后在每个任务完成后继续补充下一项,实现最大并发限制为 4。
    • 父任务取消时,group.cancelAll() 会取消所有子任务;每个子任务在开始与关键点检查 Task.isCancelled 以尽早退出。
  • 关键点2:超时与缓存
    • 通过 URLSessionConfiguration.timeoutIntervalForRequest = 5 设置每个请求的超时为 5 秒,超时则抛出 URLError.timedOut
    • 缓存使用 NSCache,并由 actor ImageCache 封装,避免多线程同时读写导致的数据竞争。
    • 缩放使用 UIGraphicsImageRenderer,按目标尺寸等比缩放,避免使用已废弃 API。
  • 关键点3:UI 回调与健壮性
    • 进度与完成回调均通过 MainActor.run {} 在主线程执行,确保安全更新 UI。
    • 单个下载失败不会影响其他任务,结果中包含该项的 error 供业务端判断。
    • 提供 cancel() 方法取消当前批次;在视图消失时调用,避免资源浪费。

适用场景

  • 列表或网格视图需要批量加载网络图片并显示缩略图
  • 需要限流(如最大并发数为 4)以避免网络与内存压力
  • 需要统一的下载、缩放、缓存策略,并安全更新 UI
  • 需要支持用户中途取消下载任务或在页面切换时释放资源

示例详情

📖 如何使用

30秒出活:复制 → 粘贴 → 搞定
与其花几十分钟和AI聊天、试错,不如直接复制这些经过千人验证的模板,修改几个 {{变量}} 就能立刻获得专业级输出。省下来的时间,足够你轻松享受两杯咖啡!
加载中...
💬 不会填参数?让 AI 反过来问你
不确定变量该填什么?一键转为对话模式,AI 会像资深顾问一样逐步引导你,问几个问题就能自动生成完美匹配你需求的定制结果。零门槛,开口就行。
转为对话模式
🚀 告别复制粘贴,Chat 里直接调用
无需切换,输入 / 唤醒 8000+ 专家级提示词。 插件将全站提示词库深度集成于 Chat 输入框。基于当前对话语境,系统智能推荐最契合的 Prompt 并自动完成参数化,让海量资源触手可及,从此彻底告别"手动搬运"。
即将推出
🔌 接口一调,提示词自己会进化
手动跑一次还行,跑一百次呢?通过 API 接口动态注入变量,接入批量评价引擎,让程序自动迭代出更高质量的提示词方案。Prompt 会自己进化,你只管收结果。
发布 API
🤖 一键变成你的专属 Agent 应用
不想每次都配参数?把这条提示词直接发布成独立 Agent,内嵌图片生成、参数优化等工具,分享链接就能用。给团队或客户一个"开箱即用"的完整方案。
创建 Agent

✅ 特性总结

按中文需求一键生成可用Swift代码,涵盖界面、网络与数据场景,显著压缩起步时间
自动补充注释与异常处理,边界清晰,降低遗漏风险,便于团队直接对接与扩展
内建规范化结构与命名建议,输出即贴合平台最佳实践,减少返工与审查成本
可指定复杂度与应用场景,灵活生成示例级或工程级实现,匹配不同阶段需求
沉淀为团队模板与复用片段,常见模块随取随用,帮助新人快速融入项目风格
同步给出优化与替代思路,从并发到性能细节提示改进方向,助力版本加速
严格规避违规与过时做法,避免审核红线与安全隐患,让功能上线更加安心
支持多轮补充和澄清,持续理解上下文,自动完善方案,产出更贴近真实业务

🎯 解决的问题

把“我想实现的功能”快速变成可落地的 Swift 代码方案,服务于iOS研发的通用与高频场景(界面搭建、数据读写、网络请求、并发处理、性能优化等)。通过专业角色与严格约束,生成可直接粘贴使用的代码片段与注释说明,兼顾稳定性、可读性与扩展性,帮助:

  • 独立开发者与小团队:缩短从需求到可运行代码的时间,减少试错与返工
  • 企业移动团队:统一实现风格与质量标准,降低评审成本与上线风险
  • 学习者与转岗者:在真实场景中边学边用,快速构建知识与经验 目标成效:显著缩短迭代周期、减少低级错误、规避过时做法与审核风险,让每次输出都更“能跑、能审、能维护”。立即试用:只需给出功能描述、开发场景与期望复杂度,即刻获得高质量Swift代码片段与要点解析。

🕒 版本历史

当前版本
v2.1 2024-01-15
优化输出结构,增强情节连贯性
  • ✨ 新增章节节奏控制参数
  • 🔧 优化人物关系描述逻辑
  • 📝 改进主题深化引导语
  • 🎯 增强情节转折点设计
v2.0 2023-12-20
重构提示词架构,提升生成质量
  • 🚀 全新的提示词结构设计
  • 📊 增加输出格式化选项
  • 💡 优化角色塑造引导
v1.5 2023-11-10
修复已知问题,提升稳定性
  • 🐛 修复长文本处理bug
  • ⚡ 提升响应速度
v1.0 2023-10-01
首次发布
  • 🎉 初始版本上线
COMING SOON
版本历史追踪,即将启航
记录每一次提示词的进化与升级,敬请期待。

💬 用户评价

4.8
⭐⭐⭐⭐⭐
基于 28 条评价
5星
85%
4星
12%
3星
3%
👤
电商运营 - 张先生
⭐⭐⭐⭐⭐ 2025-01-15
双十一用这个提示词生成了20多张海报,效果非常好!点击率提升了35%,节省了大量设计时间。参数调整很灵活,能快速适配不同节日。
效果好 节省时间
👤
品牌设计师 - 李女士
⭐⭐⭐⭐⭐ 2025-01-10
作为设计师,这个提示词帮我快速生成创意方向,大大提升了工作效率。生成的海报氛围感很强,稍作调整就能直接使用。
创意好 专业
COMING SOON
用户评价与反馈系统,即将上线
倾听真实反馈,在这里留下您的使用心得,敬请期待。
加载中...
📋
提示词复制
在当前页面填写参数后直接复制: