
- 在yana/Utils中新增ImageCacheManager类,提供内存和磁盘缓存功能,支持图片的异步加载和预加载。 - 更新FeedView,使用优化后的动态卡片组件OptimizedDynamicCardView,集成图片缓存,提升用户体验。 - 在yana/yana-Bridging-Header.h中引入CommonCrypto以支持MD5哈希。 - 更新FeedFeature以增加动态请求的页面大小,提升数据加载效率。 - 删除不再使用的data.txt文件,保持项目整洁。
227 lines
6.5 KiB
Swift
227 lines
6.5 KiB
Swift
import SwiftUI
|
||
import UIKit
|
||
import Combine
|
||
|
||
// MARK: - 图片缓存管理器
|
||
@MainActor
|
||
class ImageCacheManager: ObservableObject {
|
||
static let shared = ImageCacheManager()
|
||
|
||
private let memoryCache = NSCache<NSString, UIImage>()
|
||
private let diskCache = DiskImageCache()
|
||
private let urlSession: URLSession
|
||
|
||
// 正在下载的任务
|
||
private var downloadTasks: [String: Task<UIImage?, Never>] = [:]
|
||
|
||
private init() {
|
||
// 配置内存缓存
|
||
memoryCache.countLimit = 100 // 最多缓存100张图片
|
||
memoryCache.totalCostLimit = 50 * 1024 * 1024 // 50MB内存限制
|
||
|
||
// 配置URLSession
|
||
let config = URLSessionConfiguration.default
|
||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||
config.urlCache = URLCache(
|
||
memoryCapacity: 20 * 1024 * 1024, // 20MB内存缓存
|
||
diskCapacity: 100 * 1024 * 1024, // 100MB磁盘缓存
|
||
diskPath: "image_cache"
|
||
)
|
||
self.urlSession = URLSession(configuration: config)
|
||
}
|
||
|
||
/// 获取图片(优先从缓存获取)
|
||
func getImage(from url: String) async -> UIImage? {
|
||
let cacheKey = NSString(string: url)
|
||
|
||
// 1. 检查内存缓存
|
||
if let cachedImage = memoryCache.object(forKey: cacheKey) {
|
||
return cachedImage
|
||
}
|
||
|
||
// 2. 检查磁盘缓存
|
||
if let diskImage = await diskCache.getImage(for: url) {
|
||
// 存入内存缓存
|
||
memoryCache.setObject(diskImage, forKey: cacheKey)
|
||
return diskImage
|
||
}
|
||
|
||
// 3. 检查是否已经在下载中
|
||
if let existingTask = downloadTasks[url] {
|
||
return await existingTask.value
|
||
}
|
||
|
||
// 4. 开始下载
|
||
let downloadTask = Task<UIImage?, Never> {
|
||
await downloadImage(from: url)
|
||
}
|
||
|
||
downloadTasks[url] = downloadTask
|
||
let image = await downloadTask.value
|
||
downloadTasks.removeValue(forKey: url)
|
||
|
||
return image
|
||
}
|
||
|
||
/// 预加载图片
|
||
func preloadImages(urls: [String]) {
|
||
Task {
|
||
await withTaskGroup(of: Void.self) { group in
|
||
for url in urls {
|
||
group.addTask {
|
||
_ = await self.getImage(from: url)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 下载图片
|
||
private func downloadImage(from urlString: String) async -> UIImage? {
|
||
guard let url = URL(string: urlString) else { return nil }
|
||
|
||
do {
|
||
let (data, _) = try await urlSession.data(from: url)
|
||
guard let image = UIImage(data: data) else { return nil }
|
||
|
||
// 存入缓存
|
||
let cacheKey = NSString(string: urlString)
|
||
memoryCache.setObject(image, forKey: cacheKey)
|
||
|
||
// 存入磁盘缓存
|
||
await diskCache.setImage(image, for: urlString)
|
||
|
||
return image
|
||
} catch {
|
||
print("图片下载失败: \(error)")
|
||
return nil
|
||
}
|
||
}
|
||
|
||
/// 清理缓存
|
||
func clearCache() {
|
||
memoryCache.removeAllObjects()
|
||
Task {
|
||
await diskCache.clearCache()
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 磁盘缓存
|
||
private actor DiskImageCache {
|
||
private let cacheDirectory: URL
|
||
|
||
init() {
|
||
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||
cacheDirectory = documentsPath.appendingPathComponent("ImageCache")
|
||
|
||
// 创建缓存目录
|
||
try? FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
|
||
}
|
||
|
||
func getImage(for url: String) async -> UIImage? {
|
||
let fileName: String
|
||
if #available(iOS 13.0, *) {
|
||
fileName = url.sha256()
|
||
} else {
|
||
fileName = url.md5()
|
||
}
|
||
let fileURL = cacheDirectory.appendingPathComponent(fileName)
|
||
|
||
guard FileManager.default.fileExists(atPath: fileURL.path),
|
||
let data = try? Data(contentsOf: fileURL),
|
||
let image = UIImage(data: data) else {
|
||
return nil
|
||
}
|
||
|
||
return image
|
||
}
|
||
|
||
func setImage(_ image: UIImage, for url: String) async {
|
||
let fileName: String
|
||
if #available(iOS 13.0, *) {
|
||
fileName = url.sha256()
|
||
} else {
|
||
fileName = url.md5()
|
||
}
|
||
let fileURL = cacheDirectory.appendingPathComponent(fileName)
|
||
|
||
guard let data = image.jpegData(compressionQuality: 0.8) else { return }
|
||
try? data.write(to: fileURL)
|
||
}
|
||
|
||
func clearCache() async {
|
||
try? FileManager.default.removeItem(at: cacheDirectory)
|
||
try? FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
|
||
}
|
||
}
|
||
|
||
// MARK: - 缓存图片组件
|
||
struct CachedAsyncImage<Content: View, Placeholder: View>: View {
|
||
let url: String
|
||
let content: (Image) -> Content
|
||
let placeholder: () -> Placeholder
|
||
|
||
@State private var image: UIImage?
|
||
@State private var isLoading = false
|
||
|
||
init(
|
||
url: String,
|
||
@ViewBuilder content: @escaping (Image) -> Content,
|
||
@ViewBuilder placeholder: @escaping () -> Placeholder
|
||
) {
|
||
self.url = url
|
||
self.content = content
|
||
self.placeholder = placeholder
|
||
}
|
||
|
||
var body: some View {
|
||
Group {
|
||
if let image = image {
|
||
content(Image(uiImage: image))
|
||
} else {
|
||
placeholder()
|
||
.onAppear {
|
||
loadImage()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func loadImage() {
|
||
guard !isLoading else { return }
|
||
isLoading = true
|
||
|
||
Task {
|
||
let loadedImage = await ImageCacheManager.shared.getImage(from: url)
|
||
await MainActor.run {
|
||
self.image = loadedImage
|
||
self.isLoading = false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 便利构造器
|
||
extension CachedAsyncImage where Content == Image, Placeholder == Color {
|
||
init(url: String) {
|
||
self.init(
|
||
url: url,
|
||
content: { $0 },
|
||
placeholder: { Color.gray.opacity(0.3) }
|
||
)
|
||
}
|
||
}
|
||
|
||
extension CachedAsyncImage where Placeholder == Color {
|
||
init(
|
||
url: String,
|
||
@ViewBuilder content: @escaping (Image) -> Content
|
||
) {
|
||
self.init(
|
||
url: url,
|
||
content: content,
|
||
placeholder: { Color.gray.opacity(0.3) }
|
||
)
|
||
}
|
||
} |