热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
本提示词专为iOS开发场景设计,能够根据用户描述的功能需求生成高质量的Swift代码片段。通过角色扮演和专业约束,确保输出的代码符合iOS开发最佳实践,包含必要的注释说明和错误处理机制。适用于各类iOS应用开发、功能实现和代码优化场景,帮助开发者快速获取精准的技术解决方案。
使用 SwiftUI 构建一个简洁的登录界面,采用 MVVM 结构与可注入的验证/网络层。支持:
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) // 动态字体预览
}
}
}
实现一个基于 URLSession 的 REST 客户端,用于请求 /articles 列表(GET),支持分页与关键字查询。代码包含:
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:网络层抽象与可测试性
关键点2:缓存策略与 ETag
关键点3:重试与错误分类
关键点4:超时与线程安全
关键点5:解码与模型
实现一个使用 Swift 并发(TaskGroup)进行图片批量下载的组件:
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
}
}
group.cancelAll() 会取消所有子任务;每个子任务在开始与关键点检查 Task.isCancelled 以尽早退出。URLSessionConfiguration.timeoutIntervalForRequest = 5 设置每个请求的超时为 5 秒,超时则抛出 URLError.timedOut。ImageCache 封装,避免多线程同时读写导致的数据竞争。UIGraphicsImageRenderer,按目标尺寸等比缩放,避免使用已废弃 API。MainActor.run {} 在主线程执行,确保安全更新 UI。error 供业务端判断。cancel() 方法取消当前批次;在视图消失时调用,避免资源浪费。把“我想实现的功能”快速变成可落地的 Swift 代码方案,服务于iOS研发的通用与高频场景(界面搭建、数据读写、网络请求、并发处理、性能优化等)。通过专业角色与严格约束,生成可直接粘贴使用的代码片段与注释说明,兼顾稳定性、可读性与扩展性,帮助:
输入想做的功能即可得到带注释示例,迅速理解写法与边界处理,完成课程作业和练手应用
快速搭建页面、网络与数据模块,少查资料即可完成核心功能并提交审核,加快版本发布
在迭代中用作功能骨架生成器,统一命名与注释,补齐异常处理,稳妥接入现有工程
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
半价获取高级提示词-优惠即将到期