9

I’d like to create an animated GIF in SwiftUI.

I tried:

  1. Adding a GIF to my assets and loading that through Image(“myGif”)
  2. Creating a UIImage through downloading a GIF image, and passing that to Image
  3. Using this script to create a UIImage and then repeating #2.

None of the above worked, has anyone figured this out?

Will
  • 4,942
  • 2
  • 22
  • 47
  • 1
    iOS does not display animated gifs so why would using SwiftUI change that? – matt Jul 14 '19 at 04:46
  • 1
    I'm a actually not talking about iOS, I'm talking about the Apple Watch (hence the 'apple-watch' tag). WatchKit provides a very easy interface for displaying GIFs, I'm trying to recreate that in SwiftUI – Will Jul 14 '19 at 07:26

1 Answers1

8

You can do it by creating UIViewRepresentable from UIView. UIViewRepresentable create View for you, which you can use it in SwiftUI class.

Step1: Create UIView class for Gifview

class GIFPlayerView: UIView {
    private let imageView = UIImageView()

    convenience init(gifName: String) { 
       self.init()
       let gif = UIImage.gif(asset: gifName)
       imageView.image = gif
       imageView.contentMode = .scaleAspectFit
       self.addSubview(imageView)
    }

    override init(frame: CGRect) {
       super.init(frame: frame)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        imageView.frame = bounds
    }
}

Step2: Create UIViewRepresentable class for GifView

struct GIFView: UIViewRepresentable {
    var gifName: String

    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<GIFView>) {

    }


    func makeUIView(context: Context) -> UIView {
        return GIFPlayerView(gifName: gifName)
    }
}

Step3: Use that UIViewRepresentable view.

var body: some View {
    VStack {
        GIFView(gifName: "your gif name")
    }
}

Note: Extension for Gif to Image. Refer: https://stackoverflow.com/a/45554784

extension UIImageView {

    public func loadGif(name: String) {
        DispatchQueue.global().async {
            let image = UIImage.gif(name: name)
            DispatchQueue.main.async {
                self.image = image
            }
        }
    }

    @available(iOS 9.0, *)
    public func loadGif(asset: String) {
        DispatchQueue.global().async {
            let image = UIImage.gif(asset: asset)
            DispatchQueue.main.async {
                self.image = image
            }
        }
    }

}

extension UIImage {

    public class func gif(data: Data) -> UIImage? {
        // Create source from data
        guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
            print("SwiftGif: Source for the image does not exist")
            return nil
        }

        return UIImage.animatedImageWithSource(source)
    }

    public class func gif(url: String) -> UIImage? {
        // Validate URL
        guard let bundleURL = URL(string: url) else {
            print("SwiftGif: This image named \"\(url)\" does not exist")
            return nil
        }

        // Validate data
        guard let imageData = try? Data(contentsOf: bundleURL) else {
            print("SwiftGif: Cannot turn image named \"\(url)\" into NSData")
            return nil
        }

        return gif(data: imageData)
    }

    public class func gif(name: String) -> UIImage? {
        // Check for existance of gif
        guard let bundleURL = Bundle.main
          .url(forResource: name, withExtension: "gif") else {
            print("SwiftGif: This image named \"\(name)\" does not exist")
            return nil
        }

        // Validate data
        guard let imageData = try? Data(contentsOf: bundleURL) else {
            print("SwiftGif: Cannot turn image named \"\(name)\" into NSData")
            return nil
        }

        return gif(data: imageData)
    }

    @available(iOS 9.0, *)
    public class func gif(asset: String) -> UIImage? {
        // Create source from assets catalog
        guard let dataAsset = NSDataAsset(name: asset) else {
            print("SwiftGif: Cannot turn image named \"\(asset)\" into NSDataAsset")
            return nil
        }

        return gif(data: dataAsset.data)
    }

    internal class func delayForImageAtIndex(_ index: Int, source: CGImageSource!) -> Double {
        var delay = 0.1

        // Get dictionaries
        let cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil)
        let gifPropertiesPointer = UnsafeMutablePointer<UnsafeRawPointer?>.allocate(capacity: 0)
        defer {
            gifPropertiesPointer.deallocate()
        }
        let unsafePointer = Unmanaged.passUnretained(kCGImagePropertyGIFDictionary).toOpaque()
        if CFDictionaryGetValueIfPresent(cfProperties, unsafePointer, gifPropertiesPointer) == false {
            return delay
        }

        let gifProperties: CFDictionary = unsafeBitCast(gifPropertiesPointer.pointee, to: CFDictionary.self)

        // Get delay time
        var delayObject: AnyObject = unsafeBitCast(
            CFDictionaryGetValue(gifProperties,
                Unmanaged.passUnretained(kCGImagePropertyGIFUnclampedDelayTime).toOpaque()),
            to: AnyObject.self)
        if delayObject.doubleValue == 0 {
            delayObject = unsafeBitCast(CFDictionaryGetValue(gifProperties,
                Unmanaged.passUnretained(kCGImagePropertyGIFDelayTime).toOpaque()), to: AnyObject.self)
        }

        if let delayObject = delayObject as? Double, delayObject > 0 {
            delay = delayObject
        } else {
            delay = 0.1 // Make sure they're not too fast
        }

        return delay
    }

    internal class func gcdForPair(_ lhs: Int?, _ rhs: Int?) -> Int {
        var lhs = lhs
        var rhs = rhs
        // Check if one of them is nil
        if rhs == nil || lhs == nil {
            if rhs != nil {
                return rhs!
            } else if lhs != nil {
                return lhs!
            } else {
                return 0
            }
        }

        // Swap for modulo
        if lhs! < rhs! {
            let ctp = lhs
            lhs = rhs
            rhs = ctp
        }

        // Get greatest common divisor
        var rest: Int
        while true {
            rest = lhs! % rhs!

            if rest == 0 {
                return rhs! // Found it
            } else {
                lhs = rhs
                rhs = rest
            }
        }
    }

    internal class func gcdForArray(_ array: [Int]) -> Int {
        if array.isEmpty {
            return 1
        }

        var gcd = array[0]

        for val in array {
            gcd = UIImage.gcdForPair(val, gcd)
        }

        return gcd
    }

    internal class func animatedImageWithSource(_ source: CGImageSource) -> UIImage? {
        let count = CGImageSourceGetCount(source)
        var images = [CGImage]()
        var delays = [Int]()

        // Fill arrays
        for index in 0..<count {
            // Add image
            if let image = CGImageSourceCreateImageAtIndex(source, index, nil) {
                images.append(image)
            }

            // At it's delay in cs
            let delaySeconds = UIImage.delayForImageAtIndex(Int(index),
                source: source)
            delays.append(Int(delaySeconds * 1000.0)) // Seconds to ms
        }

        // Calculate full duration
        let duration: Int = {
            var sum = 0

            for val: Int in delays {
                sum += val
            }

            return sum
            }()

        // Get frames
        let gcd = gcdForArray(delays)
        var frames = [UIImage]()

        var frame: UIImage
        var frameCount: Int
        for index in 0..<count {
            frame = UIImage(cgImage: images[Int(index)])
            frameCount = Int(delays[Int(index)] / gcd)

            for _ in 0..<frameCount {
                frames.append(frame)
            }
        }

        // Heyhey
        let animation = UIImage.animatedImage(with: frames,
            duration: Double(duration) / 1000.0)

        return animation
    }

}
Vishal Patel
  • 717
  • 6
  • 11
  • Sorry, I should’ve made it more clear in my post. This is for WatchOS. I solved the problem by taking a similar approach and using a WkInterfaceInlineMovie – Will Sep 12 '19 at 07:13
  • how use extension with swiftui ? – q8yas Nov 12 '19 at 04:30
  • Which component's extension you are asking about? @q8yas – Vishal Patel Nov 12 '19 at 05:36
  • hi Vishal Patel , thank you for Response , the extension you put – q8yas Nov 13 '19 at 00:27
  • extension UIImageView { public func loadGif(name: String) { DispatchQueue.global().async { let image = UIImage.gif(name: name) DispatchQueue.main.async { self.image = image } } } ........................ – q8yas Nov 13 '19 at 00:27
  • @q8yas, You can not directly use that extension in swiftUI component. you have to create SwiftUI "View" with "UIViewRepresentable".Inside that UIViewRepresentable you have to pass custom UIView. In that custom UIView you can add UIImageview, In that UIImageview you can access that "extension". – Vishal Patel Nov 13 '19 at 04:28
  • @q8yas, If you can read the answer carefully, All these steps are provided there. – Vishal Patel Nov 13 '19 at 04:29
  • can i use extension with out Step1 & 2 & 3 ? – q8yas Nov 13 '19 at 07:03
  • Nope, You can't use UIkit's components extension in the SwiftUi component. You must have to create a bridge to use UIKit component in SwiftUI. Step 1,2 & 3 are bridge. – Vishal Patel Nov 13 '19 at 09:41
  • Hi bro , thank you its work , but how can use multui time in my project like GIFView("gif1") GIFView("gif2") can that ? – q8yas Nov 14 '19 at 01:01
  • Sorry, I didn't understand your question. Could you please post it with a proper description? – Vishal Patel Nov 14 '19 at 04:45
  • i want use it like this var body: some View { VStack { GIFView("pic1.gif") GIFView("pic2.gif") } } – q8yas Nov 14 '19 at 04:47
  • you have to save pic1.gif and pic2.gif in the project folder. It will work as you want – Vishal Patel Nov 14 '19 at 06:27
  • when put GifView("pic1.gif") in contentview say Argument passed to call that takes no arguments – q8yas Nov 14 '19 at 06:39
  • @q8yas I have updated my answer. It's now based on Argument passed to GifView. – Vishal Patel Nov 14 '19 at 10:38
  • @VishalPatel is it possible to run the gif only once? – Ktt May 06 '20 at 09:12