热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
本提示词专为iOS开发者设计,能够根据特定功能或组件信息,生成结构完整、内容专业的README文档章节。通过系统化的分析流程,确保输出内容技术准确、逻辑清晰,涵盖功能描述、使用说明、代码示例等核心要素,帮助开发者快速创建高质量的文档,提升项目可维护性和团队协作效率。适用于新功能介绍、组件文档编写、技术方案说明等多种iOS开发场景。
一个支持异步图片加载、内存/磁盘双缓存与失效策略的轻量组件。适用于瀑布流、Feed、头像等高频图片场景,提供预取与缓存清理 API,同时支持 SwiftUI 与 UIKit 集成。目标平台 iOS 13+。
AsyncImageCache 基于 URLSession 和 NSCache 实现高性能图片加载与缓存:
适用场景:
通过 Swift Package Manager 集成
注意:请将示例仓库地址替换为实际仓库 URL。
最低系统版本 iOS 13+
权限与网络
import Foundation
import UIKit
import SwiftUI
import CryptoKit
import ImageIO
// MARK: - Core
public final class AsyncImageCache {
public struct Configuration {
public var maxConcurrentDownloads: Int // 最大并发下载数
public var memoryCountLimit: Int // 内存缓存数量限制(NSCache countLimit)
public var diskCapacityMB: Int // 磁盘容量(MB,超出后依策略清理)
public var diskExpiration: TimeInterval // 磁盘缓存过期时间(秒)
public var retryCount: Int // 网络重试次数
public var retryDelay: TimeInterval // 重试间隔(秒)
public var decodePreferredSize: CGSize? // 下采样解码目标尺寸(像素按屏幕scale换算)
public static let `default` = Configuration(
maxConcurrentDownloads: 6,
memoryCountLimit: 500,
diskCapacityMB: 512,
diskExpiration: 60 * 60 * 24 * 7, // 7 天
retryCount: 2,
retryDelay: 0.5,
decodePreferredSize: CGSize(width: 1024, height: 1024)
)
public init(
maxConcurrentDownloads: Int = 6,
memoryCountLimit: Int = 500,
diskCapacityMB: Int = 512,
diskExpiration: TimeInterval = 60 * 60 * 24 * 7,
retryCount: Int = 2,
retryDelay: TimeInterval = 0.5,
decodePreferredSize: CGSize? = CGSize(width: 1024, height: 1024)
) {
self.maxConcurrentDownloads = maxConcurrentDownloads
self.memoryCountLimit = memoryCountLimit
self.diskCapacityMB = diskCapacityMB
self.diskExpiration = diskExpiration
self.retryCount = retryCount
self.retryDelay = retryDelay
self.decodePreferredSize = decodePreferredSize
}
}
public static let shared = AsyncImageCache()
private let memory = NSCache<NSURL, UIImage>()
private let diskDirectory: URL
private let diskQueue = DispatchQueue(label: "AsyncImageCache.disk")
private let session: URLSession
private let config: Configuration
public init(configuration: Configuration = .default) {
self.config = configuration
memory.countLimit = configuration.memoryCountLimit
// Create disk directory
let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
self.diskDirectory = caches.appendingPathComponent("AsyncImageCache", isDirectory: true)
try? FileManager.default.createDirectory(at: diskDirectory, withIntermediateDirectories: true)
// URLSession with concurrency control
let sessionConfig = URLSessionConfiguration.default
sessionConfig.requestCachePolicy = .reloadIgnoringLocalCacheData
sessionConfig.httpMaximumConnectionsPerHost = configuration.maxConcurrentDownloads
sessionConfig.waitsForConnectivity = true
self.session = URLSession(configuration: sessionConfig)
}
// Public API
public func image(for url: URL, bypassCache: Bool = false) async throws -> UIImage {
let key = url as NSURL
// Memory cache
if !bypassCache, let img = memory.object(forKey: key) {
return img
}
// Disk cache
if !bypassCache, let data = try? readFromDisk(url: url), let img = decodeImage(data: data) {
memory.setObject(img, forKey: key)
return img
}
// Network
let data = try await download(url: url)
if let img = decodeImage(data: data) {
memory.setObject(img, forKey: key)
writeToDisk(data: data, url: url)
return img
} else {
throw NSError(domain: "AsyncImageCache", code: -1, userInfo: [NSLocalizedDescriptionKey: "Image decode failed"])
}
}
public func prefetch(urls: [URL]) async {
await withTaskGroup(of: Void.self) { group in
for url in urls {
group.addTask { _ = try? await self.image(for: url) }
}
}
}
public func clearMemory() {
memory.removeAllObjects()
}
public func clearDisk(expiredOnly: Bool = true) {
diskQueue.async {
let fm = FileManager.default
let resourceKeys: Set<URLResourceKey> = [.contentModificationDateKey, .totalFileSizeKey]
guard let files = try? fm.contentsOfDirectory(at: self.diskDirectory, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles) else { return }
let now = Date()
var totalBytes = 0
// Calculate total size and collect expired
var toRemove: [URL] = []
for file in files {
let values = try? file.resourceValues(forKeys: resourceKeys)
let mod = values?.contentModificationDate ?? now
let expired = now.timeIntervalSince(mod) > self.config.diskExpiration
if expired || !expiredOnly {
toRemove.append(file)
}
totalBytes += (values?.totalFileSize ?? 0)
}
// Capacity-based trimming if needed
let capacityBytes = self.config.diskCapacityMB * 1024 * 1024
if totalBytes > capacityBytes {
// Sort by oldest first
let sorted = files.sorted {
let a = (try? $0.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? now
let b = (try? $1.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? now
return (a ?? now) < (b ?? now)
}
var bytes = totalBytes
for file in sorted where bytes > capacityBytes {
if let size = (try? file.resourceValues(forKeys: [.totalFileSizeKey]).totalFileSize) {
try? fm.removeItem(at: file)
bytes -= size ?? 0
}
}
}
// Remove expired or all
toRemove.forEach { try? fm.removeItem(at: $0) }
}
}
public func remove(url: URL) {
diskQueue.async {
let fm = FileManager.default
let path = self.fileURL(for: url)
try? fm.removeItem(at: path)
}
memory.removeObject(forKey: url as NSURL)
}
// MARK: - Internals
private func download(url: URL) async throws -> Data {
var lastError: Error?
for attempt in 0...config.retryCount {
do {
let (data, resp) = try await session.data(from: url)
if let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) {
return data
} else {
throw NSError(domain: "AsyncImageCache", code: (resp as? HTTPURLResponse)?.statusCode ?? -1, userInfo: [NSLocalizedDescriptionKey: "HTTP error"])
}
} catch {
lastError = error
if attempt < config.retryCount {
try await Task.sleep(nanoseconds: UInt64(config.retryDelay * 1_000_000_000))
continue
}
}
}
throw lastError ?? NSError(domain: "AsyncImageCache", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unknown download error"])
}
private func decodeImage(data: Data) -> UIImage? {
if let target = config.decodePreferredSize {
let maxPixel = Int(max(target.width, target.height) * UIScreen.main.scale)
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return UIImage(data: data) }
let options: [NSString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxPixel,
kCGImageSourceShouldCacheImmediately: true
]
if let cg = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) {
return UIImage(cgImage: cg)
}
}
return UIImage(data: data)
}
private func readFromDisk(url: URL) throws -> Data? {
var result: Data?
diskQueue.sync {
let fm = FileManager.default
let path = fileURL(for: url)
guard fm.fileExists(atPath: path.path) else { return }
// Expiration check
let now = Date()
let mod = (try? fm.attributesOfItem(atPath: path.path)[.modificationDate] as? Date) ?? now
let expired = now.timeIntervalSince(mod) > config.diskExpiration
if expired {
try? fm.removeItem(at: path)
return
}
result = try? Data(contentsOf: path)
}
return result
}
private func writeToDisk(data: Data, url: URL) {
diskQueue.async {
let fm = FileManager.default
let path = self.fileURL(for: url)
do {
try data.write(to: path, options: .atomic)
// Update modification date to now
try fm.setAttributes([.modificationDate: Date()], ofItemAtPath: path.path)
} catch {
// Ignore disk write errors
}
}
}
private func fileURL(for url: URL) -> URL {
let hash = SHA256.hash(data: Data(url.absoluteString.utf8))
let name = hash.map { String(format: "%02x", $0) }.joined() + ".img"
return diskDirectory.appendingPathComponent(name)
}
}
// MARK: - SwiftUI helper
public struct AsyncCachedImage: View {
private let url: URL?
private let placeholder: Image
private let cache: AsyncImageCache
@State private var image: UIImage?
@State private var isLoading = false
public init(url: URL?, placeholder: Image = Image(systemName: "photo"), cache: AsyncImageCache = .shared) {
self.url = url
self.placeholder = placeholder
self.cache = cache
}
public var body: some View {
Group {
if let ui = image {
Image(uiImage: ui).resizable().scaledToFill()
} else {
placeholder.resizable().scaledToFit().opacity(0.3)
}
}
.onAppear {
guard !isLoading, let url else { return }
isLoading = true
Task {
defer { isLoading = false }
if let img = try? await cache.image(for: url) {
self.image = img
}
}
}
}
}
// MARK: - UIKit helper
public extension UIImageView {
func setImage(
from url: URL?,
placeholder: UIImage? = nil,
cache: AsyncImageCache = .shared
) {
self.image = placeholder
guard let url else { return }
Task { [weak self] in
guard let self else { return }
if let img = try? await cache.image(for: url) {
// 更新到主线程
await MainActor.run { self.image = img }
}
}
}
}
// MARK: - Usage Examples
// SwiftUI usage:
/*
struct FeedCell: View {
let avatarURL: URL?
var body: some View {
HStack {
AsyncCachedImage(url: avatarURL, placeholder: Image(systemName: "person.crop.circle"))
.frame(width: 40, height: 40)
.clipShape(Circle())
Text("Username")
Spacer()
}
}
}
*/
// UIKit usage:
/*
class AvatarCell: UICollectionViewCell {
let avatar = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
avatar.contentMode = .scaleAspectFill
avatar.clipsToBounds = true
avatar.layer.cornerRadius = 20
contentView.addSubview(avatar)
avatar.frame = CGRect(x: 10, y: 10, width: 40, height: 40)
}
required init?(coder: NSCoder) { fatalError() }
func configure(url: URL?) {
avatar.setImage(from: url, placeholder: UIImage(systemName: "person.crop.circle"))
}
}
*/
// Prefetch & cache management:
/*
let urls: [URL] = [...] // 即将显示的图片链接
Task { await AsyncImageCache.shared.prefetch(urls: urls) }
// 清理内存缓存
AsyncImageCache.shared.clearMemory()
// 清理过期磁盘缓存
AsyncImageCache.shared.clearDisk(expiredOnly: true)
// 移除特定条目
if let u = URL(string: "https://example.com/image.jpg") {
AsyncImageCache.shared.remove(url: u)
}
*/
以上内容为 AsyncImageCache 的基础 README 章节,可根据实际仓库结构补充更多扩展接口与高级用法说明。
基于 SwiftUI Environment 的主题管理组件,统一管理颜色、字体、圆角等视觉变量,支持动态深色模式与高对比显示,可运行时切换、持久化与远端配置。适用于多品牌皮肤与大型项目的视觉一致性建设。目标平台:iOS 14+,集成复杂度:简单。
ThemeKit 通过 SwiftUI 的 Environment 和 EnvironmentObject 将主题作为一等公民贯穿视图层,提供统一的设计令牌(Design Tokens)访问方式。开发者可在不入侵业务代码的前提下,按品牌或场景定义主题包并在运行时切换;同时支持将用户选择持久化,并可接入远端配置以实现灰度与个性化方案。
适用场景:
import SwiftUI
// MARK: - Design Tokens
struct ColorSet {
let light: Color
let dark: Color
let highContrastLight: Color?
let highContrastDark: Color?
func resolved(colorScheme: ColorScheme, contrast: AccessibilityContrast) -> Color {
switch (colorScheme, contrast) {
case (.light, .increased):
return highContrastLight ?? light
case (.dark, .increased):
return highContrastDark ?? dark
case (.light, _):
return light
case (.dark, _):
return dark
@unknown default:
return light
}
}
}
struct Colors {
let primary: ColorSet
let background: ColorSet
let text: ColorSet
}
struct Typography {
let title: Font
let body: Font
let caption: Font
}
struct CornerRadius {
let small: CGFloat
let medium: CGFloat
let large: CGFloat
}
// MARK: - Theme Protocol
protocol Theme {
var id: String { get }
var colors: Colors { get }
var typography: Typography { get }
var cornerRadius: CornerRadius { get }
}
// 示例主题:系统默认主题
struct SystemTheme: Theme {
let id = "system"
let colors = Colors(
primary: ColorSet(
light: Color.blue,
dark: Color.blue.opacity(0.85),
highContrastLight: Color(hue: 0.61, saturation: 1.0, brightness: 0.95),
highContrastDark: Color(hue: 0.61, saturation: 1.0, brightness: 0.85)
),
background: ColorSet(
light: Color(UIColor.systemBackground),
dark: Color(UIColor.systemBackground),
highContrastLight: Color.white,
highContrastDark: Color.black
),
text: ColorSet(
light: Color.primary,
dark: Color.primary,
highContrastLight: Color.black,
highContrastDark: Color.white
)
)
let typography = Typography(
title: .system(.title, design: .rounded).weight(.semibold),
body: .system(.body, design: .default),
caption: .system(.caption, design: .default)
)
let cornerRadius = CornerRadius(small: 6, medium: 10, large: 16)
}
// 示例主题:品牌 A
struct BrandATheme: Theme {
let id = "brandA"
let colors = Colors(
primary: ColorSet(
light: Color(hex: "#0055FF"),
dark: Color(hex: "#77A8FF"),
highContrastLight: Color(hex: "#0033CC"),
highContrastDark: Color(hex: "#99BBFF")
),
background: ColorSet(
light: Color(hex: "#F7F9FC"),
dark: Color(hex: "#0B0F14"),
highContrastLight: Color.white,
highContrastDark: Color.black
),
text: ColorSet(
light: Color(hex: "#0A1F44"),
dark: Color(hex: "#E6EDF7"),
highContrastLight: Color.black,
highContrastDark: Color.white
)
)
let typography = Typography(
title: .system(size: 24, weight: .bold, design: .rounded),
body: .system(size: 17, weight: .regular, design: .default),
caption: .system(size: 13, weight: .regular, design: .default)
)
let cornerRadius = CornerRadius(small: 4, medium: 8, large: 12)
}
// MARK: - Environment Key
private struct ThemeKey: EnvironmentKey {
static let defaultValue: Theme = SystemTheme()
}
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
// MARK: - Persistence Strategy
enum PersistenceStrategy {
case none
case userDefaults(key: String)
func save(themeID: String) {
switch self {
case .none: break
case .userDefaults(let key):
UserDefaults.standard.set(themeID, forKey: key)
}
}
func restoredThemeID() -> String? {
switch self {
case .none: return nil
case .userDefaults(let key):
return UserDefaults.standard.string(forKey: key)
}
}
}
// MARK: - Theme Manager
final class ThemeManager: ObservableObject {
@Published private(set) var current: Theme
private let persistence: PersistenceStrategy
init(defaultTheme: Theme = SystemTheme(),
persistence: PersistenceStrategy = .none) {
self.persistence = persistence
if let restoredID = persistence.restoredThemeID(),
let restoredTheme = ThemeManager.resolveTheme(id: restoredID) {
self.current = restoredTheme
} else {
self.current = defaultTheme
}
}
func apply(_ theme: Theme) {
current = theme
persistence.save(themeID: theme.id)
}
// 注册或从远端解析后的主题可在此处扩展解析
static func resolveTheme(id: String) -> Theme? {
switch id {
case "system": return SystemTheme()
case "brandA": return BrandATheme()
default: return nil
}
}
// MARK: - Remote Config (示例)
struct RemoteThemeDTO: Decodable {
struct ColorPair: Decodable {
let light: String
let dark: String
let highContrastLight: String?
let highContrastDark: String?
}
struct ColorsDTO: Decodable {
let primary: ColorPair
let background: ColorPair
let text: ColorPair
}
struct CornerDTO: Decodable {
let small: CGFloat
let medium: CGFloat
let large: CGFloat
}
let id: String
let colors: ColorsDTO
let cornerRadius: CornerDTO
// 字体远端下发通常涉及命名与尺寸,示例省略或采用系统默认
}
func loadRemoteTheme(from url: URL,
completion: @escaping (Result<Theme, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
completion(.failure(error)); return
}
guard let data = data else {
completion(.failure(NSError(domain: "ThemeKit", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Empty data"])))
return
}
do {
let dto = try JSONDecoder().decode(RemoteThemeDTO.self, from: data)
let theme = Self.theme(from: dto)
completion(.success(theme))
} catch {
completion(.failure(error))
}
}.resume()
}
private static func theme(from dto: RemoteThemeDTO) -> Theme {
let colors = Colors(
primary: ColorSet(
light: Color(hex: dto.colors.primary.light),
dark: Color(hex: dto.colors.primary.dark),
highContrastLight: dto.colors.primary.highContrastLight.map(Color.init(hex:)),
highContrastDark: dto.colors.primary.highContrastDark.map(Color.init(hex:))
),
background: ColorSet(
light: Color(hex: dto.colors.background.light),
dark: Color(hex: dto.colors.background.dark),
highContrastLight: dto.colors.background.highContrastLight.map(Color.init(hex:)),
highContrastDark: dto.colors.background.highContrastDark.map(Color.init(hex:))
),
text: ColorSet(
light: Color(hex: dto.colors.text.light),
dark: Color(hex: dto.colors.text.dark),
highContrastLight: dto.colors.text.highContrastLight.map(Color.init(hex:)),
highContrastDark: dto.colors.text.highContrastDark.map(Color.init(hex:))
)
)
let corners = CornerRadius(
small: dto.cornerRadius.small,
medium: dto.cornerRadius.medium,
large: dto.cornerRadius.large
)
// 远端未指定字体时采用系统默认
let typography = Typography(
title: .system(.title, design: .rounded).weight(.semibold),
body: .system(.body, design: .default),
caption: .system(.caption, design: .default)
)
return GenericTheme(id: dto.id, colors: colors, typography: typography, cornerRadius: corners)
}
}
// 通用主题封装,便于远端解析
struct GenericTheme: Theme {
let id: String
let colors: Colors
let typography: Typography
let cornerRadius: CornerRadius
}
// MARK: - Color Hex Support
extension Color {
init(hex: String) {
let cleaned = hex.replacingOccurrences(of: "#", with: "")
var value: UInt64 = 0
Scanner(string: cleaned).scanHexInt64(&value)
let r, g, b: Double
let a: Double = 1.0
switch cleaned.count {
case 6:
r = Double((value & 0xFF0000) >> 16) / 255.0
g = Double((value & 0x00FF00) >> 8) / 255.0
b = Double(value & 0x0000FF) / 255.0
default:
r = 0; g = 0; b = 0
}
self = Color(.sRGB, red: r, green: g, blue: b, opacity: a)
}
}
// MARK: - App Integration
@main
struct ExampleApp: App {
@StateObject private var themeManager = ThemeManager(
defaultTheme: SystemTheme(),
persistence: .userDefaults(key: "ThemeKit.currentThemeID")
)
var body: some Scene {
WindowGroup {
ContentView()
// 运行时切换会触发视图重建,Environment 跟随更新
.environment(\.theme, themeManager.current)
.environmentObject(themeManager)
}
}
}
// MARK: - Usage in Views
struct ContentView: View {
@Environment(\.theme) private var theme
@Environment(\.colorScheme) private var colorScheme
@Environment(\.accessibilityContrast) private var contrast
@EnvironmentObject private var themeManager: ThemeManager
var body: some View {
VStack(spacing: 16) {
Text("ThemeKit for SwiftUI")
.font(theme.typography.title)
.foregroundColor(theme.colors.text.resolved(colorScheme: colorScheme, contrast: contrast))
RoundedRectangle(cornerRadius: theme.cornerRadius.medium)
.fill(theme.colors.primary.resolved(colorScheme: colorScheme, contrast: contrast))
.frame(height: 56)
.overlay(Text("Primary").font(theme.typography.body).foregroundColor(.white))
HStack {
Button("系统主题") { themeManager.apply(SystemTheme()) }
Button("品牌 A") { themeManager.apply(BrandATheme()) }
}
.buttonStyle(.borderedProminent)
}
.padding()
.background(theme.colors.background.resolved(colorScheme: colorScheme, contrast: contrast))
}
}
// MARK: - Remote Theme Load Example
struct RemoteThemeLoaderView: View {
@EnvironmentObject private var themeManager: ThemeManager
@State private var loading = false
@State private var errorMessage: String?
var body: some View {
VStack(spacing: 12) {
Button(loading ? "加载中..." : "从远端加载主题") {
guard !loading else { return }
loading = true
let url = URL(string: "https://example.com/theme.json")!
themeManager.loadRemoteTheme(from: url) { result in
DispatchQueue.main.async {
loading = false
switch result {
case .success(let theme):
themeManager.apply(theme)
case .failure(let error):
errorMessage = error.localizedDescription
}
}
}
}
if let errorMessage = errorMessage {
Text(errorMessage).foregroundColor(.red).font(.caption)
}
}
.padding()
}
}
.environment(\.theme, themeManager.current) 放在根视图或导航根节点处,确保 @Published 变更触发视图重建并传播环境值。@Environment(\.accessibilityContrast) 和 @Environment(\.colorScheme) 获取当前设置,颜色应通过 ColorSet.resolved(...) 适配。Font.system,实际项目若需自定义字体,请确保资源可用并与动态类型兼容(PreferredContentSizeCategory)。resolved 中执行重计算或 I/O。封装 LocalAuthentication 的生物识别授权管理器,支持 Face ID / Touch ID 验证及回退密码策略,统一错误映射与提示文案,提供 async/await 与回调双API、Keychain 安全绑定与会话超时控制。适用于敏感操作授权、账号解锁、支付确认等场景。
BiometricAuthManager 提供一套一致、可配置的生物识别授权解决方案:
适用场景:
import Foundation
import LocalAuthentication
import Security
import UIKit
public enum BiometryType {
case none, touchID, faceID
}
public enum FallbackStrategy {
// 不提供回退
case none
// 使用系统设备密码(Device Passcode)
case systemPasscode
// 自定义回退(例如应用内密码)。当用户选择“输入密码”或生物识别不可用时回调。
case custom(handler: (_ presenting: UIViewController?) -> Void, title: String? = nil)
}
public struct BiometricAuthConfig {
// 系统弹窗中的理由文案(Localized Reason)
public var localizedReason: String
// 取消按钮标题(可选)
public var cancelTitle: String?
// 指纹失败后显示的“输入密码”标题(仅在 .deviceOwnerAuthenticationWithBiometrics 下生效)
public var fallbackTitle: String?
// 回退策略
public var fallback: FallbackStrategy
// 会话超时时长(秒)。在窗口期内可免验证(基于应用策略)
public var sessionTimeout: TimeInterval
// 复用时长(秒)。在规定时间内可复用最近成功验证(依赖 LAContext 复用机制)
public var allowableReuseDuration: TimeInterval?
// 是否仅允许生物识别(true 时不触发系统设备密码,仅生物识别)
public var biometryOnly: Bool
public init(localizedReason: String,
cancelTitle: String? = nil,
fallbackTitle: String? = nil,
fallback: FallbackStrategy = .systemPasscode,
sessionTimeout: TimeInterval = 120,
allowableReuseDuration: TimeInterval? = nil,
biometryOnly: Bool = false) {
self.localizedReason = localizedReason
self.cancelTitle = cancelTitle
self.fallbackTitle = fallbackTitle
self.fallback = fallback
self.sessionTimeout = sessionTimeout
self.allowableReuseDuration = allowableReuseDuration
self.biometryOnly = biometryOnly
}
}
public enum BiometricAuthError: Error {
case biometryNotAvailable
case biometryNotEnrolled
case biometryLockout
case devicePasscodeNotSet
case authFailed
case cancelledByUser
case cancelledBySystem
case appCancelled
case fallbackRequested // 触发自定义回退
case keychainFailed(OSStatus)
case unknown(Error?)
// 面向用户的建议性提示(示例,可根据产品本地化)
public var message: String {
switch self {
case .biometryNotAvailable:
return "此设备不支持生物识别。"
case .biometryNotEnrolled:
return "未设置 Face ID/Touch ID,请前往设置完成注册。"
case .biometryLockout:
return "多次尝试失败,生物识别已锁定,请使用设备密码解锁后重试。"
case .devicePasscodeNotSet:
return "未设置设备解锁密码,请先在设置中开启。"
case .authFailed:
return "验证失败,请重试。"
case .cancelledByUser:
return "已取消验证。"
case .cancelledBySystem:
return "系统取消了验证,请重试。"
case .appCancelled:
return "验证已被应用取消。"
case .fallbackRequested:
return "请使用备用验证方式。"
case .keychainFailed:
return "安全存储失败,请重试。"
case .unknown:
return "发生未知错误,请重试。"
}
}
}
public struct BiometricAuthResult {
public let biometryType: BiometryType
public let usedFallback: Bool
}
public protocol LAContextProviding {
func makeContext() -> LAContext
}
public final class DefaultLAContextProvider: LAContextProviding {
public init() {}
public func makeContext() -> LAContext { LAContext() }
}
// Keychain 安全存储(绑定生物识别/设备密码)
final class KeychainStore {
private let service = "com.example.biometric.session"
private let account = "BiometricSessionToken"
// 创建 SecAccessControl:生物识别/用户存在绑定
// biometryOnly = true -> .biometryCurrentSet
// biometryOnly = false -> .userPresence(允许设备密码或生物识别)
private func makeAccessControl(biometryOnly: Bool) throws -> SecAccessControl {
var error: Unmanaged<CFError>?
let flags: SecAccessControlCreateFlags = biometryOnly ? .biometryCurrentSet : .userPresence
guard let ac = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
flags,
&error
) else {
throw error?.takeRetainedValue() as Error? ?? BiometricAuthError.keychainFailed(errSecParam)
}
return ac
}
func ensureTokenExists(biometryOnly: Bool) throws {
// 若已存在则返回
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnAttributes as String: true
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
if status == errSecSuccess { return }
guard status == errSecItemNotFound else { throw BiometricAuthError.keychainFailed(status) }
// 随机令牌
var token = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, token.count, &token)
// 新增,使用访问控制
let ac = try makeAccessControl(biometryOnly: biometryOnly)
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecAttrAccessControl as String: ac,
kSecValueData as String: Data(token)
]
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else { throw BiometricAuthError.keychainFailed(addStatus) }
}
// 读取令牌会触发 LA 验证(需要传入 LAContext)
func readToken(with context: LAContext) throws -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecUseAuthenticationContext as String: context,
kSecReturnData as String: true
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let data = item as? Data else {
throw BiometricAuthError.keychainFailed(status)
}
return data
}
}
public final class BiometricAuthManager {
public static let shared = BiometricAuthManager()
private let contextProvider: LAContextProviding
private let keychain = KeychainStore()
private var lastSuccessAt: Date?
private let lastAuthQueue = DispatchQueue(label: "biometric.lastAuth.queue")
public init(contextProvider: LAContextProviding = DefaultLAContextProvider()) {
self.contextProvider = contextProvider
}
public func biometryType() -> BiometryType {
let ctx = contextProvider.makeContext()
var error: NSError?
let _ = ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
switch ctx.biometryType {
case .faceID: return .faceID
case .touchID: return .touchID
default: return .none
}
}
public func canEvaluateBiometrics() -> Result<BiometryType, BiometricAuthError> {
let ctx = contextProvider.makeContext()
var err: NSError?
let can = ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &err)
if can { return .success(biometryType()) }
return .failure(mapLAError(err))
}
private func isSessionValid(timeout: TimeInterval) -> Bool {
return lastAuthQueue.sync {
guard let last = self.lastSuccessAt else { return false }
return Date().timeIntervalSince(last) < timeout
}
}
private func markSessionNow() {
lastAuthQueue.sync { self.lastSuccessAt = Date() }
}
// 回调式 API(iOS 12+)
public func authenticate(config: BiometricAuthConfig,
presentingViewController: UIViewController? = nil,
completion: @escaping (Result<BiometricAuthResult, BiometricAuthError>) -> Void) {
// 会话窗口期内免验证(应用级别策略)
if isSessionValid(timeout: config.sessionTimeout) {
let result = BiometricAuthResult(biometryType: biometryType(), usedFallback: false)
return completion(.success(result))
}
// 确保 Keychain Token 已建立(访问控制)
do { try keychain.ensureTokenExists(biometryOnly: config.biometryOnly) }
catch { return completion(.failure((error as? BiometricAuthError) ?? .unknown(error))) }
let ctx = contextProvider.makeContext()
ctx.localizedCancelTitle = config.cancelTitle
if case .systemPasscode = config.fallback {
// 使用 .deviceOwnerAuthentication 允许设备密码
ctx.localizedFallbackTitle = config.fallbackTitle ?? "输入密码"
} else if case .custom(_, let title) = config.fallback {
ctx.localizedFallbackTitle = title ?? "其它方式"
} else {
ctx.localizedFallbackTitle = "" // 隐藏“输入密码”按钮
}
if let reuse = config.allowableReuseDuration {
ctx.touchIDAuthenticationAllowableReuseDuration = reuse
}
// 选择策略
let policy: LAPolicy = {
if config.biometryOnly { return .deviceOwnerAuthenticationWithBiometrics }
switch config.fallback {
case .systemPasscode: return .deviceOwnerAuthentication
default: return .deviceOwnerAuthenticationWithBiometrics
}
}()
var canError: NSError?
guard ctx.canEvaluatePolicy(policy, error: &canError) else {
return completion(.failure(mapLAError(canError)))
}
// 发起验证(读取 Keychain 触发生物识别/密码)
ctx.evaluatePolicy(policy, localizedReason: config.localizedReason) { [weak self] success, error in
guard success else {
// 用户点击“输入密码”按钮,在 .deviceOwnerAuthenticationWithBiometrics 下会回调 .userFallback
if let laErr = error as? LAError, laErr.code == .userFallback {
switch config.fallback {
case .custom(let handler, _):
DispatchQueue.main.async {
handler(presentingViewController)
completion(.failure(.fallbackRequested))
}
return
default:
// 其它策略已在 policy 中处理
break
}
}
return completion(.failure(mapLAError(error as NSError?)))
}
// 尝试读取绑定 Token(确保确实通过了 LA / Passcode)
do {
_ = try self?.keychain.readToken(with: ctx)
self?.markSessionNow()
let result = BiometricAuthResult(biometryType: self?.biometryType() ?? .none, usedFallback: policy == .deviceOwnerAuthentication)
completion(.success(result))
} catch {
completion(.failure((error as? BiometricAuthError) ?? .unknown(error)))
}
}
}
// async/await API(iOS 13+ 回溯)
@available(iOS 13.0, *)
public func authenticateAsync(config: BiometricAuthConfig,
presentingViewController: UIViewController? = nil) async throws -> BiometricAuthResult {
try await withCheckedThrowingContinuation { cont in
self.authenticate(config: config, presentingViewController: presentingViewController) { result in
switch result {
case .success(let ok): cont.resume(returning: ok)
case .failure(let err): cont.resume(throwing: err)
}
}
}
}
}
// 错误映射
private func mapLAError(_ error: NSError?) -> BiometricAuthError {
guard let error = error, error.domain == LAError.errorDomain else {
return .unknown(error)
}
let code = LAError.Code(rawValue: error.code) ?? .authenticationFailed
switch code {
case .biometryNotAvailable: return .biometryNotAvailable
case .biometryNotEnrolled: return .biometryNotEnrolled
case .biometryLockout: return .biometryLockout
case .passcodeNotSet: return .devicePasscodeNotSet
case .userCancel: return .cancelledByUser
case .systemCancel: return .cancelledBySystem
case .appCancel: return .appCancelled
case .authenticationFailed: return .authFailed
case .userFallback: return .fallbackRequested
default: return .unknown(error)
}
}
// MARK: - 使用示例(ViewController)
final class PayConfirmViewController: UIViewController {
private let auth = BiometricAuthManager.shared
@IBAction func confirmButtonTapped(_ sender: Any) {
let config = BiometricAuthConfig(
localizedReason: "请验证以确认支付",
cancelTitle: "取消",
fallbackTitle: "输入设备密码",
fallback: .systemPasscode,
sessionTimeout: 120, // 2 分钟内免验证
allowableReuseDuration: 10, // 系统级复用 10 秒
biometryOnly: false
)
if #available(iOS 13.0, *) {
Task {
do {
let result = try await auth.authenticateAsync(config: config, presentingViewController: self)
self.showAlert(title: "验证成功", message: "方式:\(result.biometryType == .faceID ? "Face ID" : (result.biometryType == .touchID ? "Touch ID" : "未知"))")
// TODO: 执行支付
} catch let err as BiometricAuthError {
self.handleError(err)
} catch {
self.showAlert(title: "错误", message: error.localizedDescription)
}
}
} else {
auth.authenticate(config: config, presentingViewController: self) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let ok):
self?.showAlert(title: "验证成功", message: "方式:\(ok.biometryType == .faceID ? "Face ID" : (ok.biometryType == .touchID ? "Touch ID" : "未知"))")
// TODO: 执行支付
case .failure(let e):
self?.handleError(e)
}
}
}
}
}
private func handleError(_ error: BiometricAuthError) {
switch error {
case .biometryNotEnrolled, .devicePasscodeNotSet:
// 引导用户前往设置
let alert = UIAlertController(title: "需要设置", message: error.message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "前往设置", style: .default, handler: { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}))
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
present(alert, animated: true)
case .fallbackRequested:
// 如果使用 .custom(handler:),此处一般无需处理;示例中用系统密码,则不会到这里
break
default:
showAlert(title: "验证未完成", message: error.message)
}
}
private func showAlert(title: String, message: String) {
let a = UIAlertController(title: title, message: message, preferredStyle: .alert)
a.addAction(UIAlertAction(title: "好的", style: .default))
present(a, animated: true)
}
}
用最少的输入,快速产出可直接提交到仓库的iOS组件README章节:覆盖功能价值、适用场景、特性亮点、集成步骤、Swift代码示例、注意事项与参考链接。一方面,帮助个人开发者在发布与开源时展现专业水准;另一方面,为团队建立统一的文档标准,减少沟通与评审成本,加速代码合并与成员上手,推动知识沉淀与复用,最终以更低成本实现更高质量的交付与品牌形象。
为新UI组件、网络模块等快速产出README;补齐使用示例与集成步骤;在提测或合并前一次性完善文档。
制定统一文档模板并批量应用到各模块;在评审前生成标准说明,保障交付一致性与可维护性。
用清晰的README展示核心价值与上手路径;提供可复制代码示例,降低用户集成门槛,提升项目口碑。
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
半价获取高级提示词-优惠即将到期