Kamel-Media / Kamel

Kotlin asynchronous media loading and caching library for Compose.
Apache License 2.0
657 stars 25 forks source link

Support for Loading Images on iOS in KMP Project #113

Open hubercode opened 2 months ago

hubercode commented 2 months ago

Problem Description:

I am working on a Kotlin Multiplatform (KMP) project where I need to load an image from the device’s file system. While the implementation works perfectly on Android, I am encountering issues on iOS where the image does not display. The path to the image seems correct, as I can verify the image exists on the iOS device’s file system. However, it does not render in the KamelImage.

Expected Behavior:

The image should load and display correctly on both Android and iOS devices using the same KMP codebase.

Actual Behavior:

• Android: Image loads and displays correctly. • iOS: Image does not display, even though the file path is correct.

Code Example:

Here is the relevant portion of my implementation:

val painterResource = loadImageResource(data = uiModel.imageFilePath)
KamelImage(
    painterResource,
    contentDescription = null,
    modifier = Modifier
        .fillMaxWidth()
        .clip(
            shape = RoundedCornerShape(10.dp)
        ),
    contentScale = ContentScale.Crop
)
@Composable
expect fun loadImageResource(data: String): Resource<Painter>

@Composable // iOS
actual fun loadImageResource(data: String): Resource<Painter> {
    val nsUrl = NSURL.fileURLWithPath(data)
    return asyncPainterResource(data = nsUrl)
}

@Composable // Android
actual fun loadImageResource(data: String): Resource<Painter> {
    return asyncPainterResource(data = File(data))
}

Example FileName:

iOS: /Users/michael/Library/Developer/CoreSimulator/Devices/45D9A2D0-C80D-46E1-BC61-7AC048B75D47/data/Containers/Data/Application/9DD527DF-5093-46E5-BC6E-CB8B1FFBA835/Documents/story/images/img-3EdLqeuRN51sTaOcAvWMPFgW.png

Andorid: /data/user/0/ch.huber.storki/files/story/images/img-3EdLqeuRN51sTaOcAvWMPFgW.png

Request:

Could you provide guidance on how to properly load images from the file system on iOS using KamelImage? Are there any known issues or additional steps needed to ensure compatibility on iOS?

Any assistance or advice would be greatly appreciated. Thank you for your hard work on this project!

luca992 commented 2 months ago

Are you getting any useful error in onFailure?

hubercode commented 2 months ago

Thank you for your quick response. I get this IllegalStateException:

kotlin.IllegalStateException:` Unable to find a fetcher for class NSURL
    at 0   app-kmp                          0x10121490b        kfun:kotlin.Throwable#<init>(kotlin.String?){} + 119 
    at 1   app-kmp                          0x10120def7        kfun:kotlin.Exception#<init>(kotlin.String?){} + 115 
    at 2   app-kmp                          0x10120e117        kfun:kotlin.RuntimeException#<init>(kotlin.String?){} + 115 
    at 3   app-kmp                          0x10120e6b7        kfun:kotlin.IllegalStateException#<init>(kotlin.String?){} + 115 
    at 4   app-kmp                          0x1022f2cd3        kfun:io.kamel.core.utils#findFetcherFor__at__io.kamel.core.config.KamelConfig(0:0){0§<kotlin.Any>}io.kamel.core.fetcher.Fetcher<0:0> + 987 
    at 5   app-kmp                          0x1022d9a43        kfun:io.kamel.core.loadImageBitmapResource$lambda$0#internal + 855 
    at 6   app-kmp                          0x1022de4b7        kfun:io.kamel.core.$loadImageBitmapResource$lambda$0$FUNCTION_REFERENCE$0.invoke#internal + 151 
    at 7   app-kmp                          0x10135a903        kfun:kotlin.coroutines.SuspendFunction1#invoke#suspend(1:0;kotlin.coroutines.Continuation<1:1>){}kotlin.Any?-trampoline + 115 
    at 8   app-kmp                          0x101425b3f        kfun:kotlinx.coroutines.flow.SafeFlow.collectSafely#internal + 199 
    at 9   app-kmp                          0x10148b22f        kfun:kotlinx.coroutines.flow.AbstractFlow#collectSafely#suspend(kotlinx.coroutines.flow.FlowCollector<1:0>;kotlin.coroutines.Continuation<kotlin.Unit>){}kotlin.Any-trampoline + 75 
    at 10  app-kmp                          0x101427c6b        kfun:kotlinx.coroutines.flow.AbstractFlow.$collectCOROUTINE$0.invokeSuspend#internal + 639 
    at 11  app-kmp                          0x101427f3b        kfun:kotlinx.coroutines.flow.AbstractFlow#collect#suspend(kotlinx.coroutines.flow.FlowCollector<1:0>;kotlin.coroutines.Continuation<kotlin.Unit>){}kotlin.Any + 295 
    at 12  app-kmp                          0x10148b16b        kfun:kotlinx.coroutines.flow.Flow#collect#suspend(kotlinx.coroutines.flow.FlowCollector<1:0>;kotlin.coroutines.Continuation<kotlin.Unit>){}kotlin.Any-trampoline + 115 
    at 13  app-kmp                          0x10144453f        kfun:kotlinx.coroutines.flow.$catchImplCOROUTINE$0.invokeSuspend#internal + 723 
    at 14  app-kmp                          0x10144495b        kfun:kotlinx.coroutines.flow#catchImpl#suspend__at__kotlinx.coroutines.flow.Flow<0:0>(kotlinx.coroutines.flow.FlowCollector<0:0>;kotlin.coroutines.Continuation<kotlin.Throwable?>){0§<kotlin.Any?>}kotlin.Any? + 295 
    at 15  app-kmp                          0x101445573        kfun:kotlinx.coroutines.flow.object-18.$collectCOROUTINE$2.invokeSuspend#internal + 575 
    at 16  app-kmp                          0x10144593f        kfun:kotlinx.coroutines.flow.object-18.collect#internal + 295 
    at 17  app-kmp                          0x10148b16b        kfun:kotlinx.coroutines.flow.Flow#collect#suspend(kotlinx.coroutines.flow.FlowCollector<1:0>;kotlin.coroutines.Continuation<kotlin.Unit>){}kotlin.Any-trampoline + 115 
    at 18  app-kmp                          0x10166aff7        kfun:androidx.compose.runtime.collectAsState$lambda$3$lambda$2#internal + 251 
    at 19  app-kmp                          0x10166d367        kfun:androidx.compose.runtime.$collectAsState$lambda$3$lambda$2$FUNCTION_REFERENCE$4.invoke#internal + 139 
    at 20  app-kmp                          0x10135a557        kfun:kotlin.Function2#invoke(1:0;1:1){}1:2-trampoline + 115 
    at 21  app-kmp                          0x10121dbdf        kfun:kotlin.coroutines.intrinsics.object-4.invokeSuspend#internal + 731 
    at 22  app-kmp                          0x101359e73        kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any?-trampoline + 67 
    at 23  app-kmp                          0x10121a34b        kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 623 
    at 24  app-kmp                          0x101359f53        kfun:kotlin.coroutines.Continuation#resumeWith(kotlin.Result<1:0>){}-trampoline + 99 
    at 25  app-kmp                          0x1014580cb        kfun:kotlinx.coroutines.DispatchedTask#run(){} + 1879 
    at 26  app-kmp                          0x101486647        kfun:kotlinx.coroutines.Runnable#run(){}-trampoline + 91 
    at 27  app-kmp                          0x10145aacf        kfun:kotlinx.coroutines.internal.LimitedDispatcher.Worker.run#internal + 231 
    at 28  app-kmp                          0x101486647        kfun:kotlinx.coroutines.Runnable#run(){}-trampoline + 91 
    at 29  app-kmp                          0x10147db4f        kfun:kotlinx.coroutines.MultiWorkerDispatcher.$workerRunLoop$lambda$2COROUTINE$0.invokeSuspend#internal + 2095 
    at 30  app-kmp                          0x101359e73        kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any?-trampoline + 67 
    at 31  app-kmp                          0x10121a34b        kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 623 
    at 32  app-kmp                          0x101359f53        kfun:kotlin.coroutines.Continuation#resumeWith(kotlin.Result<1:0>){}-trampoline + 99 
    at 33  app-kmp                          0x1014580cb        kfun:kotlinx.coroutines.DispatchedTask#run(){} + 1879 
    at 34  app-kmp                          0x101486647        kfun:kotlinx.coroutines.Runnable#run(){}-trampoline + 91 
    at 35  app-kmp                          0x1013e8837        kfun:kotlinx.coroutines.EventLoopImplBase#processNextEvent(){}kotlin.Long + 1247 
    at 36  app-kmp                          0x101486393        kfun:kotlinx.coroutines.EventLoop#processNextEvent(){}kotlin.Long-trampoline + 51 
    at 37  app-kmp                          0x1014770bb        kfun:kotlinx.coroutines.BlockingCoroutine.joinBlocking#internal + 435 
    at 38  app-kmp                          0x101475f1f        kfun:kotlinx.coroutines#runBlocking(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>){0§<kotlin.Any?>}0:0 + 1395 
    at 39  app-kmp                          0x1014760ff        kfun:kotlinx.coroutines#runBlocking$default(kotlin.coroutines.CoroutineContext?;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>;kotlin.Int){0§<kotlin.Any?>}0:0 + 239 
    at 40  app-kmp                          0x10147bd33        kfun:kotlinx.coroutines.MultiWorkerDispatcher.workerRunLoop#internal + 183 
    at 41  app-kmp                          0x10147cf9b        kfun:kotlinx.coroutines.MultiWorkerDispatcher.<init>$lambda$1$lambda$0#internal + 67 
    at 42  app-kmp                          0x10147e297        kfun:kotlinx.coroutines.MultiWorkerDispatcher.$<init>$lambda$1$lambda$0$FUNCTION_REFERENCE$5.invoke#internal + 71 
    at 43  app-kmp                          0x10147e367        kfun:kotlinx.coroutines.MultiWorkerDispatcher.$<init>$lambda$1$lambda$0$FUNCTION_REFERENCE$5.$<bridge-UNN>invoke(){}#internal + 71 
    at 44  app-kmp                          0x10135680f        kfun:kotlin.Function0#invoke(){}1:0-trampoline + 99 
    at 45  app-kmp                          0x101226d5f        WorkerLaunchpad + 131 
    at 46  app-kmp                          0x1013b7857        _ZN6Worker19processQueueElementEb + 2635 
    at 47  app-kmp                          0x1013b6c97        _ZN12_GLOBAL__N_113workerRoutineEPv + 219 
    at 48  libsystem_pthread.dylib             0x1056db413        _pthread_start + 103 
    at 49  libsystem_pthread.dylib             0x1056d65df        thread_start + 7 
luca992 commented 2 months ago

can you try wrapping it in File

hubercode commented 2 months ago

Can you provide me a quick example what you mean by "wrapping it in File"? I may I tried to wrap the path (data) in File, but this results in the following exception when building for iOS:

Caused by: java.lang.IllegalStateException: FIELD name:io_kamel_core_utils_File$stable type:kotlin.Int visibility:public [final,static]

@Composable // iOS
actual fun loadImageResource(data: String): Resource<Painter> {
//    val nsUrl = NSURL.fileURLWithPath(data)
    val file = File(data)
    return asyncPainterResource(data = file) // exception gets thrown here
}

Thank you!

luca992 commented 2 months ago

Here's the example in the sample:

https://github.com/Kamel-Media/Kamel/blob/01c4809e95e01ba50bdca5be65001e4bd605125c/kamel-samples/src/commonMain/kotlin/io/kamel/samples/FileSample.kt

https://github.com/Kamel-Media/Kamel/blob/01c4809e95e01ba50bdca5be65001e4bd605125c/kamel-samples/src/iosMain/kotlin/io/kamel/samples/getResourceFile.ios.kt

It probably has to do with your path though. I don't think you can directly access files on your drive by absolute path from an ios simulator. Try bundling it as a resource like the sample does. This is probably relevant as well:

https://stackoverflow.com/questions/37574689/how-to-load-image-from-local-path-ios-swift-by-path