
- Comprehensive README update documenting v2.0 architectural changes - Updated git remote to ssh://rockvilleav@git.rockvilletollandsda.church:10443/RTSDA/RTSDA-iOS.git - Documented unified ChurchService and 60% code reduction - Added new features: Home Feed, responsive reading, enhanced UI - Corrected license information (GPL v3 with church content copyright) - Updated build instructions and technical stack details
122 lines
4.1 KiB
Swift
122 lines
4.1 KiB
Swift
import SwiftUI
|
|
import Foundation
|
|
|
|
// Response structure for cached image data
|
|
struct CachedImageResponse: Codable {
|
|
let success: Bool
|
|
let data: String?
|
|
let contentType: String?
|
|
let cached: Bool?
|
|
let error: String?
|
|
}
|
|
|
|
struct CachedAsyncImage<Content: View, Placeholder: View>: View {
|
|
private let url: URL?
|
|
private let scale: CGFloat
|
|
private let content: (Image) -> Content
|
|
private let placeholder: () -> Placeholder
|
|
|
|
@State private var image: UIImage?
|
|
@State private var isLoading = false
|
|
@State private var loadError: Error?
|
|
|
|
init(
|
|
url: URL?,
|
|
scale: CGFloat = 1.0,
|
|
@ViewBuilder content: @escaping (Image) -> Content,
|
|
@ViewBuilder placeholder: @escaping () -> Placeholder
|
|
) {
|
|
self.url = url
|
|
self.scale = scale
|
|
self.content = content
|
|
self.placeholder = placeholder
|
|
}
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let image = image {
|
|
content(Image(uiImage: image))
|
|
} else if isLoading {
|
|
placeholder()
|
|
} else if loadError != nil {
|
|
placeholder()
|
|
} else {
|
|
placeholder()
|
|
}
|
|
}
|
|
.onAppear {
|
|
loadCachedImage()
|
|
}
|
|
.onChange(of: url) { _, newURL in
|
|
image = nil
|
|
loadError = nil
|
|
loadCachedImage()
|
|
}
|
|
}
|
|
|
|
private func loadCachedImage() {
|
|
guard let url = url else { return }
|
|
|
|
isLoading = true
|
|
loadError = nil
|
|
|
|
// Use background queue for image processing
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
do {
|
|
// Call Rust crate function for cached image
|
|
let jsonResponse = fetchCachedImageBase64(url: url.absoluteString)
|
|
|
|
// Parse JSON response
|
|
guard let jsonData = jsonResponse.data(using: .utf8) else {
|
|
DispatchQueue.main.async {
|
|
self.isLoading = false
|
|
self.loadError = NSError(domain: "CachedImageError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON response"])
|
|
}
|
|
return
|
|
}
|
|
|
|
let response = try JSONDecoder().decode(CachedImageResponse.self, from: jsonData)
|
|
|
|
if response.success, let base64Data = response.data {
|
|
// Decode base64 to image
|
|
if let imageData = Data(base64Encoded: base64Data),
|
|
let uiImage = UIImage(data: imageData) {
|
|
DispatchQueue.main.async {
|
|
withAnimation(.easeInOut(duration: 0.3)) {
|
|
self.image = uiImage
|
|
}
|
|
self.isLoading = false
|
|
}
|
|
} else {
|
|
DispatchQueue.main.async {
|
|
self.isLoading = false
|
|
self.loadError = NSError(domain: "CachedImageError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to decode image data"])
|
|
}
|
|
}
|
|
} else {
|
|
DispatchQueue.main.async {
|
|
self.isLoading = false
|
|
self.loadError = NSError(domain: "CachedImageError", code: 3, userInfo: [NSLocalizedDescriptionKey: response.error ?? "Unknown error"])
|
|
}
|
|
}
|
|
} catch {
|
|
DispatchQueue.main.async {
|
|
self.isLoading = false
|
|
self.loadError = error
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convenience initializer for common use case
|
|
extension CachedAsyncImage where Content == Image, Placeholder == ProgressView<EmptyView, EmptyView> {
|
|
init(url: URL?, scale: CGFloat = 1.0) {
|
|
self.init(
|
|
url: url,
|
|
scale: scale,
|
|
content: { $0 },
|
|
placeholder: { ProgressView() }
|
|
)
|
|
}
|
|
} |