¥
立即购买

Swift代码片段生成专家

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

本提示词专为iOS开发场景设计,能够根据用户描述的功能需求生成高质量的Swift代码片段。通过角色扮演和专业约束,确保输出的代码符合iOS开发最佳实践,包含必要的注释说明和错误处理机制。适用于各类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
  • 需要支持用户中途取消下载任务或在页面切换时释放资源

示例详情

解决的问题

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

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

适用用户

iOS初学者

输入想做的功能即可得到带注释示例,迅速理解写法与边界处理,完成课程作业和练手应用

独立开发者

快速搭建页面、网络与数据模块,少查资料即可完成核心功能并提交审核,加快版本发布

团队中级工程师

在迭代中用作功能骨架生成器,统一命名与注释,补齐异常处理,稳妥接入现有工程

特征总结

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

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

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

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

2. 发布为 API 接口调用

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

3. 在 MCP Client 中配置使用

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

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

您购买后可以获得什么

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

不要错过!

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

17
:
23
小时
:
59
分钟
:
59