Files
JetsonMediaIOS/Jetson Media/ui/AsyncImageView.swift
2025-08-17 22:08:25 +08:00

263 lines
10 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
///
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
}
}