263 lines
10 KiB
Swift
263 lines
10 KiB
Swift
import SwiftUI
|
||
import UIKit
|
||
|
||
/// 带缓存、并发控制、图片解密、双指选择效果和分享功能的异步图片视图
|
||
struct AsyncImageView: View {
|
||
let url: String
|
||
let nums: [Int]
|
||
let index: Int
|
||
@State private var image: UIImage?
|
||
@State private var isLoading = false
|
||
@State private var error: Error?
|
||
|
||
@State private var isSelecting = false // 是否处于选择状态
|
||
@State private var pressStartTime: Date?
|
||
@State private var longPressActive = false
|
||
@State private var showShareSheet = false // 控制分享面板显示
|
||
|
||
private static var currentlyLoadingIndices: Set<Int> = []
|
||
private static let maxConcurrentLoads = 4
|
||
private static let longPressThreshold: TimeInterval = 0.1 // 长按阈值(秒)
|
||
|
||
var body: some View {
|
||
Group {
|
||
if let image = image {
|
||
ZStack(alignment: .center) {
|
||
// 原始图片
|
||
Image(uiImage: image)
|
||
.resizable()
|
||
.aspectRatio(contentMode: .fit)
|
||
|
||
// 选择状态的视觉效果
|
||
if isSelecting {
|
||
Image(uiImage: image)
|
||
.resizable()
|
||
.aspectRatio(contentMode: .fit)
|
||
.colorMultiply(.white)
|
||
.opacity(0.7)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 4)
|
||
.stroke(Color.blue, lineWidth: 3) // 改为蓝色边框更醒目
|
||
)
|
||
.blendMode(.lighten)
|
||
|
||
// 分享按钮(仅在选择状态显示)
|
||
VStack {
|
||
Spacer()
|
||
Button(action: {
|
||
showShareSheet = true
|
||
}) {
|
||
Image(systemName: "square.and.arrow.up")
|
||
.foregroundColor(.white)
|
||
.padding()
|
||
.background(Color.blue)
|
||
.clipShape(Circle())
|
||
.shadow(radius: 4)
|
||
}
|
||
.padding()
|
||
}
|
||
}
|
||
}
|
||
// 双指手势识别
|
||
.gesture(
|
||
MagnificationGesture(minimumScaleDelta: 0)
|
||
.onChanged { _ in
|
||
handleTwoFingerGesture()
|
||
}
|
||
.onEnded { _ in
|
||
// 只有长按超过阈值才保持选择状态
|
||
let shouldKeepSelection = pressStartTime.map {
|
||
Date().timeIntervalSince($0) >= Self.longPressThreshold
|
||
} ?? false
|
||
|
||
withAnimation(.easeOut) {
|
||
isSelecting = shouldKeepSelection
|
||
}
|
||
|
||
if !shouldKeepSelection {
|
||
pressStartTime = nil
|
||
longPressActive = false
|
||
}
|
||
}
|
||
)
|
||
// 分享视图
|
||
.sheet(isPresented: $showShareSheet) {
|
||
ShareSheet(activityItems: [image]) { completed in
|
||
if completed {
|
||
withAnimation(.easeOut) {
|
||
isSelecting = false
|
||
}
|
||
pressStartTime = nil
|
||
longPressActive = false
|
||
}
|
||
}
|
||
}
|
||
} else if isLoading {
|
||
ProgressView()
|
||
.frame(height: 200)
|
||
} else if error != nil {
|
||
VStack(spacing: 8) {
|
||
Image(systemName: "exclamationmark.triangle")
|
||
.foregroundColor(.red)
|
||
Text(error?.localizedDescription ?? "未知错误")
|
||
.font(.caption)
|
||
.foregroundColor(.gray)
|
||
}
|
||
.frame(height: 200)
|
||
} else {
|
||
Color.gray.opacity(0.3)
|
||
.frame(height: 200)
|
||
}
|
||
}
|
||
.onAppear {
|
||
loadImage()
|
||
}
|
||
.onDisappear {
|
||
Self.currentlyLoadingIndices.remove(index)
|
||
}
|
||
}
|
||
|
||
// 处理双指手势
|
||
private func handleTwoFingerGesture() {
|
||
if pressStartTime == nil {
|
||
pressStartTime = Date()
|
||
} else {
|
||
// 检查是否超过长按阈值
|
||
let pressDuration = Date().timeIntervalSince(pressStartTime!)
|
||
if pressDuration >= Self.longPressThreshold && !longPressActive {
|
||
longPressActive = true
|
||
// 显示选择效果
|
||
withAnimation(.easeIn) {
|
||
isSelecting = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 图片加载核心逻辑
|
||
private func loadImage() {
|
||
let cacheKey = extractCacheKey(from: url)
|
||
|
||
// 1. 先检查缓存,如果有直接使用
|
||
if let cachedImage = ImageCacheManager.shared.getImage(for: cacheKey) {
|
||
image = cachedImage
|
||
print("从缓存加载 - 键: \(cacheKey)")
|
||
return
|
||
}
|
||
|
||
// 2. 检查并发加载限制
|
||
guard !Self.currentlyLoadingIndices.contains(index),
|
||
Self.currentlyLoadingIndices.count < Self.maxConcurrentLoads else {
|
||
return
|
||
}
|
||
|
||
// 3. 开始加载流程
|
||
Self.currentlyLoadingIndices.insert(index)
|
||
isLoading = true
|
||
error = nil
|
||
|
||
// 4. 验证解密参数
|
||
guard let decodeNum = nums[safe: index] else {
|
||
error = NSError(domain: "数据错误", code: 1, userInfo: [NSLocalizedDescriptionKey: "图片参数不匹配"])
|
||
isLoading = false
|
||
Self.currentlyLoadingIndices.remove(index)
|
||
return
|
||
}
|
||
|
||
print("调试信息:加载图片 - URL: \(url), 缓存键: \(cacheKey), 解密参数: \(decodeNum), 索引: \(index)")
|
||
|
||
// 5. 验证URL有效性
|
||
guard let imageUrl = URL(string: url) else {
|
||
error = NSError(domain: "URL错误", code: 2, userInfo: [NSLocalizedDescriptionKey: "无效的图片URL"])
|
||
isLoading = false
|
||
Self.currentlyLoadingIndices.remove(index)
|
||
return
|
||
}
|
||
|
||
// 6. 网络请求加载图片
|
||
URLSession.shared.dataTask(with: imageUrl) { data, response, error in
|
||
DispatchQueue.main.async {
|
||
// 清理加载状态
|
||
Self.currentlyLoadingIndices.remove(index)
|
||
self.isLoading = false
|
||
|
||
// 处理网络错误
|
||
if let error = error {
|
||
self.error = error
|
||
print("图片加载失败(网络错误): \(error.localizedDescription)")
|
||
return
|
||
}
|
||
|
||
// 验证响应和数据
|
||
guard let httpResponse = response as? HTTPURLResponse,
|
||
(200...299).contains(httpResponse.statusCode),
|
||
let data = data, !data.isEmpty else {
|
||
self.error = NSError(domain: "响应错误", code: 3, userInfo: [NSLocalizedDescriptionKey: "无效的服务器响应"])
|
||
print("图片加载失败(无效响应)")
|
||
return
|
||
}
|
||
|
||
// 解码图片并应用解密
|
||
guard let loadedImage = UIImage(data: data) else {
|
||
self.error = NSError(domain: "解码错误", code: 4, userInfo: [NSLocalizedDescriptionKey: "无法解析图片数据"])
|
||
print("图片加载失败(数据无效)")
|
||
return
|
||
}
|
||
|
||
// 解密并缓存图片(使用简化键)
|
||
let decodedImage = loadedImage.decodeImage(num: decodeNum)
|
||
self.image = decodedImage
|
||
ImageCacheManager.shared.saveImage(decodedImage, for: cacheKey)
|
||
print("图片加载成功(已缓存) - 键: \(cacheKey)")
|
||
}
|
||
}.resume()
|
||
}
|
||
|
||
/// 从URL中提取简化的缓存键
|
||
private func extractCacheKey(from urlString: String) -> String {
|
||
guard let url = URL(string: urlString) else {
|
||
return urlString.hashValue.description
|
||
}
|
||
|
||
let pathComponents = url.pathComponents
|
||
|
||
if let photosIndex = pathComponents.firstIndex(of: "photos"),
|
||
photosIndex + 1 < pathComponents.count,
|
||
photosIndex + 2 < pathComponents.count {
|
||
let idComponent = pathComponents[photosIndex + 1]
|
||
var fileComponent = pathComponents[photosIndex + 2]
|
||
|
||
if let dotIndex = fileComponent.lastIndex(of: ".") {
|
||
fileComponent = String(fileComponent[..<dotIndex])
|
||
}
|
||
|
||
return "\(idComponent)/\(fileComponent)"
|
||
}
|
||
|
||
return url.path.hashValue.description
|
||
}
|
||
}
|
||
|
||
// 分享视图包装器
|
||
struct ShareSheet: UIViewControllerRepresentable {
|
||
let activityItems: [Any]
|
||
let completion: (Bool) -> Void
|
||
|
||
func makeUIViewController(context: Context) -> UIActivityViewController {
|
||
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
|
||
controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
|
||
completion(completed)
|
||
}
|
||
return controller
|
||
}
|
||
|
||
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
|
||
}
|
||
|
||
// 数组安全访问扩展
|
||
extension Array {
|
||
subscript(safe index: Int) -> Element? {
|
||
return indices.contains(index) ? self[index] : nil
|
||
}
|
||
}
|