Darryl Pierce
07/04/2025, 12:25 PMBinish Mathew
07/04/2025, 1:03 PMAnonymike
07/04/2025, 3:57 PMAnonymike
07/04/2025, 4:02 PMimport kotlinx.cinterop.addressOf
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.usePinned
import platform.Foundation.NSData
import platform.Foundation.dataWithBytes
import platform.UIKit.UIImage
import platform.posix.memcpy
/**
* Convert this Data to a Kotlin ByteArray.
*/
fun NSData.toByteArray(): ByteArray = ByteArray(this.length.toInt()).apply {
usePinned {
memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length)
}
}
/**
* Convert this ByteArray to Apple NSData.
*/
fun ByteArray.toNSData(): NSData = this.usePinned {
NSData.dataWithBytes(it.addressOf(0), it.get().size.toULong())
}
/**
* Convert this ByteArray to a UIImage, with scale value.
*/
fun ByteArray.toUIImage(scale: Double): UIImage? = memScoped {
val nsData = this@toUIImage.toNSData()
UIImage.imageWithData(nsData, scale)
}
/**
* Convert this ByteArray to a UIImage.
*/
fun ByteArray.toUIImage(): UIImage? = this.toUIImage(1.0)
Darryl Pierce
07/04/2025, 4:31 PMAnonymike
07/04/2025, 4:38 PMAnonymike
07/04/2025, 4:40 PMDarryl Pierce
07/04/2025, 4:47 PMDarryl Pierce
07/04/2025, 4:47 PMAnonymike
07/04/2025, 4:54 PMDarryl Pierce
07/04/2025, 4:55 PMAnonymike
07/04/2025, 4:55 PMDarryl Pierce
07/04/2025, 4:56 PMArchiveAPI.loadFile(archiveFilename, entryFilename)
which is a suspend function that returns a ByteArray.Darryl Pierce
07/04/2025, 4:56 PMDarryl Pierce
07/04/2025, 4:57 PMAnonymike
07/04/2025, 4:58 PMImageServerResponse
, here's that extension and I'll look for a more to connect the dots.
extension ImageServiceResponse {
///
/// Convert this ImageServiceResponse to a UIImage. Adds support for Webp decoding.
/// - Returns: The resulting UIImage if decoding was successful.
///
func toUIImage() -> UIImage? {
if (type == "webp") {
return SDImageWebPCoder.shared.decodedImage(with: self.data.toNSData(), options: nil)
} else {
return self.data.toUIImage()
}
}
}
Anonymike
07/04/2025, 4:58 PMAnonymike
07/04/2025, 5:01 PMsuspend fun requestImage(image: String, params: String? = null): ImageServiceResponse?
// It returns something like this:
if (response.status == HttpStatusCode.OK) {
ImageServiceResponse(image, params, "webp", response.body<ByteArray>())
} else {
null
}
Darryl Pierce
07/04/2025, 5:03 PMvar coverImage: UIImage? {
if comicBook.pages.count > 0 {
let coverPage = (comicBook.pages[0] as! ComicPage)
Task {
let imageData = try await ArchiveAPI().loadPage(
comicFilename: comicBook.filename,
pageFilename: coverPage.filename
)
if (imageData != nil) {
return imageData!.toUIImage()
}
return nil
}
}
return nil
}
Anonymike
07/04/2025, 5:04 PMimport shared
import SwiftUI
struct AsyncImageView<Content, Content2>: View where Content: View, Content2: View {
private let imageName: String?
private let imageParams: String?
private let imageKey: String
private let transaction: Transaction?
private let contentPhase: ((AsyncImagePhase) -> Content)?
private let contentImage: ((Image) -> Content)?
private let placeholder: (() -> Content2)?
private let imageService: ImageRequestService = koin.get()
@State private var imageStatus: AsyncImagePhase = .empty
@State private var imageWasRequested: Bool = false
private let log: Logger = koin.log(name: "AsyncImageService")
init(
name: String?,
params: String? = nil,
transaction: Transaction = Transaction(),
@ViewBuilder content: @escaping (AsyncImagePhase) -> Content
) {
imageName = name
imageParams = (params?.isEmpty == true) ? nil : params
if (imageName == nil) {
imageKey = "none"
} else {
imageKey = "\(imageName!)\((imageParams == nil) ? "" : "?\(imageParams!)")"
}
self.transaction = transaction
self.contentPhase = content
self.contentImage = nil
self.placeholder = { EmptyView() as! Content2 }
}
init(
name: String?,
params: String? = nil,
@ViewBuilder content: @escaping (Image) -> Content,
@ViewBuilder placeholder: @escaping () -> Content2
) {
imageName = name
imageParams = (params?.isEmpty == true) ? nil : params
if (imageName == nil) {
imageKey = "none"
} else {
imageKey = "\(imageName!)\((imageParams == nil) ? "" : ("?" + imageParams!))"
}
self.contentImage = content
self.placeholder = placeholder
self.contentPhase = nil
self.transaction = nil
}
var body: some View {
if imageName == nil {
if contentPhase != nil {
contentPhase!(.empty)
} else if placeholder != nil {
placeholder!()
}
} else if let cached = ImageCache[imageKey] {
#if DEBUG
let _ = print("cached: \(imageKey)")
#endif
if contentPhase != nil {
contentPhase?(.success(cached))
} else if contentImage != nil {
contentImage?(cached)
}
} else {
#if DEBUG
let _ = print("request: \(imageKey)")
#endif
if contentPhase != nil {
contentPhase!(imageStatus).task { await loadImage() }
} else if contentImage != nil && placeholder != nil {
switch (imageStatus) {
case .success (let image): cacheAndRender(image: image)
case .empty: placeholder!().task { await loadImage() }
case .failure: placeholder!()
default: placeholder!().task { await loadImage() }
}
}
}
}
private func loadImage() async {
if (imageWasRequested) {
return
}
#if DEBUG
log.i(messageString: "Loading Image : \(imageKey)")
#endif
imageWasRequested = true;
guard let imageName else { imageStatus = .empty; return }
let response = try? await imageService.requestImage(image: imageName, params: imageParams)
if let imageResponse = response?.toUIImage() {
imageStatus = .success(Image(uiImage: imageResponse))
} else {
imageStatus = .failure(AsyncImageServiceError.requestFailed)
}
}
private func cacheAndRender(image: Image) -> some View {
ImageCache[imageKey] = image
return contentImage?(image)
}
private func cacheAndRender(phase: AsyncImagePhase) -> some View{
if case .success (let image) = phase {
ImageCache[imageKey] = image
}
return contentPhase?(phase)
}
}
enum AsyncImageServiceError: Error {
case requestFailed
}
fileprivate class ImageCache {
static private var cache: [String: Image] = [:]
static subscript(key: String?) -> Image? {
get {
guard let key else { return nil }
return ImageCache.cache[key]
}
set {
guard let key else { return }
ImageCache.cache[key] = newValue
}
}
}
Darryl Pierce
07/04/2025, 5:08 PMAnonymike
07/04/2025, 5:16 PMDarryl Pierce
07/04/2025, 5:58 PMclass ImageLoader: ObservableObject {
private var comicBook: ComicBook
private var pageFilename: String = ""
@Published var image: UIImage? = nil
@Published var hasImage: Bool = false
init(comicBook: ComicBook) {
self.comicBook = comicBook
if (self.comicBook.pages.count > 0) {
self.pageFilename = (self.comicBook.pages[0] as! ComicPage).filename
doLoadPage()
}
}
private func doLoadPage() {
Task {
Log().debug(
tag: TAG,
message:
"Loading cover image: \(comicBook.path):\(self.pageFilename)"
)
let imageData = try await ArchiveAPI().loadPage(
comicFilename: comicBook.path,
pageFilename: self.pageFilename
)
if imageData != nil {
self.image = imageData!.toUIImage()
self.hasImage = true
}
}
}
}
Anonymike
07/04/2025, 6:04 PM