top of page
Search

Seamless GIF Loading in Compose Multiplatform: Android & iOS Implementation

  • Writer: nimesh Vasani
    nimesh Vasani
  • Feb 18
  • 3 min read


GIFs add a dynamic touch to mobile applications, but handling them across platforms can be tricky. In this blog, we’ll explore how to load GIFs in a Compose Multiplatform (KMP/CMP) project for both Android and iOS while maintaining a clean architecture.


1. Creating a Shared GIF Composable

To ensure platform-specific implementations work seamlessly, we define an expect function in the shared/common module.

@Composable
expect fun GifImage(url: String,modifier: Modifier)

This acts as a placeholder that will be implemented separately for Android and iOS.


2. Android Implementation: Jetpack Compose with Coil3


For Android, we use Coil3, a modern image loading library that supports GIFs through the GifDecoder.

@Composable
actual fun GifImage(url: String,modifier: Modifier) {
    val context = LocalContext.current
    val imageLoader = ImageLoader.Builder(context)
        .components {
            add(GifDecoder.Factory())
        }
        .build()

    AsyncImage(
        model = ImageRequest.Builder(context)
            .data(url)
            .size(Size.ORIGINAL)
            .build(),
        contentDescription = null,
        imageLoader = imageLoader,
        modifier = modifier,
        contentScale = ContentScale.FillBounds
    )
}


Why Use Coil?

  • Efficient GIF decoding with GifDecoder.Factory()

  • Asynchronous image loading with caching

  • Jetpack Compose-friendly API



3. iOS Implementation: UIKit Integration


Since Compose does not natively support GIFs on iOS, we use UIKitView to embed a UIImageView that handles GIF playback. Here i am fetching GIF from drawable resource file.

@Composable
actual fun GifImage(url: String,modifier: Modifier) {
    UIKitView(
        modifier = modifier,
        factory = {
            val imageView = UIImageView()
            imageView.contentMode = UIViewContentMode.UIViewContentModeScaleAspectFit
            imageView.clipsToBounds = true
//here I am using drawable resource as asset folder
            val path = url.removePrefix("file://")
            val gifData = NSData.create(contentsOfFile = path)
            val gifImage = gifData?.let { UIImage.gifImageWithData(it) }

            imageView.image = gifImage
            imageView
        }
    )
}

How This Works:

  1. UIKitView allows embedding native iOS UI components.

  2. UIImageView is used to display GIFs.

  3. gifImageWithData() converts NSData into an animated UIImage.


4. Fine-Tuning GIF Playback on iOS


Copy and paste this functions for loading gif properly.

@OptIn(ExperimentalForeignApi::class)
fun UIImage.Companion.gifImageWithData(data: NSData?): UIImage? {
    return runCatching {
        val dataRef = CFBridgingRetain(data) as? CFDataRef
        val source = CGImageSourceCreateWithData(dataRef, null) ?: return null
        val count = CGImageSourceGetCount(source).toInt()
        val images = mutableListOf<CGImageRef>()
        val delays = mutableListOf<Double>()

        for (i in 0 until count) {
            val image = CGImageSourceCreateImageAtIndex(source, i.toULong(), null)
            if (image != null) {
                images.add(image)
            }

            val delaySeconds = delayForImageAtIndex(i, source)
            delays.add(delaySeconds * 400.0) // s to ms
        }

        println("images=${images.count()}")

        val duration = delays.sum()
        println("duration=$duration")

        val gcd = gcdForList(delays)
        println("gcd=$gcd")
        val frames = mutableListOf<UIImage>()

        for (i in 0 until count) {
            val frame = UIImage.imageWithCGImage(images[i])
            val frameCount = (delays[i] / gcd).toInt()
            for (f in 0 until frameCount) {
                frames.add(frame)
            }
        }
        println("frames=${frames.count()}")

        val animation = UIImage.animatedImageWithImages(frames, duration / 1000.0) ?: return null
        animation
    }.onFailure { it.printStackTrace() }.getOrNull()
}

@OptIn(ExperimentalForeignApi::class)
private fun UIImage.Companion.delayForImageAtIndex(index: Int, source: CGImageSourceRef): Double {
    var delay: Double

    val cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index.toULong(), null)
    val gifKey = (CFBridgingRelease(kCGImagePropertyGIFDictionary) as NSString).toString()
    val gifInfo =
        (CFBridgingRelease(cfProperties) as? NSDictionary)?.valueForKey(gifKey) as? NSDictionary

    delay =
        gifInfo?.valueForKey((CFBridgingRelease(kCGImagePropertyGIFDelayTime) as NSString).toString()) as? Double
            ?: 0.0

    if (delay < 0.1) {
        delay = 0.1
    }

    return delay
}

private fun UIImage.Companion.gcdForPair(_a: Int?, _b: Int?): Int {
    var a = _a
    var b = _b
    if (b == null || a == null) {
        return b ?: (a ?: 0)
    }

    if (a < b) {
        val c = a
        a = b
        b = c
    }

    var rest: Int
    while (true) {
        rest = a!! % b!!
        if (rest == 0) {
            return b
        } else {
            a = b
            b = rest
        }
    }
}

private fun UIImage.Companion.gcdForList(list: List<Double>): Double {
    if (list.isEmpty()) return 1.0
    var gcd = list[0]
    list.onEach {
        gcd = UIImage.gcdForPair(it.toInt(), gcd.toInt()).toDouble()
    }
    return gcd
}

To control GIF animation speed, we calculate frame delays and total animation duration.

  val delaySeconds = delayForImageAtIndex(i, source)
            delays.add(delaySeconds * 400.0) // s to ms

Adjust GIF Playback Speed Manually

  • The multiplication factor (400.0) can be increased or decreased to speed up or slow down GIF playback.

  • Experiment with different values to find the perfect balance.


How to Use GifImage composable

In shared/Common module, to use this GIfImage, create a screen and call function, pass value for URL if you are loading from drawable resource. If you are using URL then you need to adjust iOS side NSData object.


//change your gif file name 
 GifImage(
	url = Res.getUri("drawable/gif_2.gif"),
 	modifier = Modifier.fillMaxSize()
  )

Wrapping Up

By using platform-specific implementations, we can seamlessly load and display GIFs in a Compose Multiplatform project.

This approach ensures: ✅ Efficient GIF loading on Android with CoilNative GIF handling on iOS using UIKitViewFull control over playback speed on iOS

Got questions or suggestions? Drop them in the comments! 🚀


Does this structure work for you? Let me know if you want any refinements! 😊

 
 
 

Comments


Available for Select
freelance opportunities

Have an exciting project you need 

help with?

Send me an email or contact me via

instant message!

© 2022 by Nimesh Vasani. Mobile Developer

icons8-quotation-48.png
Screenshot 2024-03-20 at 5.53.00 PM.png

You can't connect the dots looking forward; you can only connect them looking backwards. So you have to trust that the dots will somehow connect in your future. You have to trust in something - your gut, destiny, life, karma, whatever. This approach has never let me down, and it has made all the difference in my life.

I’m as proud of many of the things we haven’t done as the things we have done. Innovation is saying no to a thousand things.

- Steve Jobs

  Founder of Apple Inc.

icons8-quotation-48.png
Screenshot 2024-03-20 at 6.01.37 PM.png

Clean code always looks like it was written by someone who cares. Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program

- Robert C Martin 

    Known For Agile Manifesto.

icons8-quotation-48.png
Screenshot 2024-03-20 at 6.01.59 PM.png

When I wrote this code, only God and I understood what I did. Now only God knows. 

 – Anonymous

bottom of page