Files
e-party-iOS/yana/Utils/ImageCacheManager.swift
edwinQQQ f686480cdc feat: 添加图片缓存系统和优化FeedView组件
- 在yana/Utils中新增ImageCacheManager类,提供内存和磁盘缓存功能,支持图片的异步加载和预加载。
- 更新FeedView,使用优化后的动态卡片组件OptimizedDynamicCardView,集成图片缓存,提升用户体验。
- 在yana/yana-Bridging-Header.h中引入CommonCrypto以支持MD5哈希。
- 更新FeedFeature以增加动态请求的页面大小,提升数据加载效率。
- 删除不再使用的data.txt文件,保持项目整洁。
2025-07-11 20:18:36 +08:00

227 lines
6.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) }
)
}
}