アイリッジ開発者ブログ

アイリッジに所属するエンジニアが技術情報を発信していきます。

SwiftUI でアニメーション GIF を動かす

はじめに

こんにちは。プロダクト開発グループの于です。

今回は、UIViewRepresentable で UIKit を活用し、SwiftUI でアニメーション GIF を動かす方法のご紹介になります。

Swift や SwiftUI では、標準で GIF アニメーションを再生できません。たとえば SwiftUI の Image に GIF ファイルを指定しても、静止画としてしか表示されず、UIImage.animatedImage を使った場合も自動再生されません。

アニメーション GIF を再生するためには、

  • サードパーティライブラリを利用する
  • アニメーション GIF を事前にフレームごとに分解し、UIImage.animatedImage で再生する

などの方法が必要です。

今回は SwiftUI で UIViewRepresentable を使い、UIKit のビューをラップして GIF ファイルをアニメーション表示できるように実装しました。

環境

  • Xcode16.0
  • iOS18.1

コード解説

GIFView の全体コード

/// SwiftUI 上で GIF を表示するための UIViewRepresentable 構造体
/// UIViewRepresentable を使うことで、UIKit の UIImageView を SwiftUI に統合できる
struct GIFView: UIViewRepresentable {
    /// 表示する GIF ファイルのローカルファイル名
    let fileName: String
    
    // SwiftUI から UIKit の UIView を作成する
    func makeUIView(context: Context) -> UIView {
        // SwiftUI の UIViewRepresentable は UIView を返す必要があるため、
        // UIImageView を直接返すより、コンテナ UIView にラップする方が柔軟
        let containerView = UIView()

        // GIF を表示する UIImageView を作成
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit   // フレーム内でアスペクト比を維持して表示
        imageView.clipsToBounds = true           // はみ出る部分は切り取る

        // Auto Layout を利用して SwiftUI の frame に従わせる
        imageView.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(imageView)

        NSLayoutConstraint.activate([
            // 左右上下をコンテナにピッタリ合わせる
            imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            imageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            imageView.topAnchor.constraint(equalTo: containerView.topAnchor),
            imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
        ])
        
        // メインスレッドで imageView に設定することでアニメーションが再生される
        loadGIF(into: imageView)
        return containerView
    }
    
    // SwiftUIで状態が更新された時に呼ばれる
    func updateUIView(_ uiView: UIView, context: Context) {
    }
    
    /// GIF のローカルファイル読み込み処理
    private func loadGIF(into imageView: UIImageView) {
        // メインバンドルから GIFファイルを取得
        guard let gifPath = Bundle.main.path(forResource: fileName, ofType: nil),
              let gifData = NSData(contentsOfFile: gifPath) as Data? else {
            print("GIF file not found: \(fileName)")
            return
        }
        
        // GIF データからアニメーション画像を生成
        guard let animatedImage = UIImage.animatedImageWithGIFData(gifData) else {
            print("Failed to create animated image from: \(fileName)")
            return
        }
        
        // UI更新はメインスレッドで
        DispatchQueue.main.async {
            imageView.image = animatedImage
        }
    }
}

// MARK: - UIImage 拡張
// GIF データを分解してアニメーションUIImageを生成する
extension UIImage {
    ...
}

全体のフロー

  1. SwiftUI 側コンポーネント(GIFView)を用意
    • UIViewRepresentable を使って UIKit のビューを SwiftUI に橋渡しする
  2. makeUIView で UI を作成
    • コンテナ UIView を作成し、その中に UIImageView を追加して Auto Layout でコンテナにフィットさせる
  3. アニメーション GIF 読み込み処理を起動
    • makeUIView 内から loadGIF(into:) を呼び、指定されたファイル名でバンドル内の アニメーション GIF を探す
  4. アニメーション GIF ファイルをデータ化
    • Bundle.main でファイルパスを取得し、Data(バイト列)として読み込む
  5. GIF データをアニメーションUIImageに変換(UIImage拡張に委譲)
    • GIF を読み込み、UIImage のアニメーション画像として生成
  6. UIImageView に設定して再生開始
    • メインスレッドで imageView.image に生成したアニメーション UIImage をセットすることで再生が始まる

アニメーションを再生するには、アニメーション GIF データを分解し、アニメーション化された UIImage を生成する処理が必要です。この処理によって、GIF アニメーションを SwiftUI 上で再生できるようになります。
次のセクションでは、GIF データからアニメーション画像を生成する処理 UIImage.animatedImageWithGIFData() について解説していきます。

GIF データのアニメーション化

// MARK: - UIImage 拡張
// GIF データを分解してアニメーションUIImageを生成する
extension UIImage {
    /// GIF データを UIImage.animatedImage として返す
    /// - Parameter data: GIF ファイルのデータ
    /// - Returns: 無限ループするアニメーションUIImage(元の GIF のループ回数設定は無視される)
    static func animatedImageWithGIFData(_ data: Data) -> UIImage? {
        // GIF データからCGImageSourceを生成
        guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }
        
        let count = CGImageSourceGetCount(source) // GIF のフレーム数
        var images: [UIImage] = []               // 各フレームをUIImage化して格納
        var duration: TimeInterval = 0           // 総再生時間
        
        // 全フレームを順番に読み込む
        for i in 0..<count {
            // i番目のフレーム画像を取得
            guard let cgImage = CGImageSourceCreateImageAtIndex(source, i, nil) else { continue }
            images.append(UIImage(cgImage: cgImage))
            
            // フレームごとの表示時間(delayTime)を取得
            if let properties = CGImageSourceCopyPropertiesAtIndex(source, i, nil) as? [CFString: Any],
               let gifProperties = properties[kCGImagePropertyGIFDictionary] as? [CFString: Any],
               let delayTime = gifProperties[kCGImagePropertyGIFDelayTime] as? Double {
                // コードの簡素化のために、今回はdelayTimeを厳密に反映せず、均等な時間で再生します
                // すべてのフレームを均等な時間だけ表示する様にするので、全体の再生時間を算出します
                duration += delayTime
            }
        }
        
        // フレームごとの表示時間が取得できない場合は、1フレーム0.1秒として再生時間を設定する
        if duration == 0 {
            duration = Double(count) * 0.1
        }
        
        // アニメーションUIImageを生成して返す
        // UIImage.animatedImageは常に無限ループで再生されます
        // 今回は GIFファイル のループ回数を無視して無限ループで表示します
        return UIImage.animatedImage(with: images, duration: duration)
    }
}

処理の流れ

  1. CGImageSource を作成
    • CGImageSourceCreateWithData でアニメーション GIF データを解析し、フレームやプロパティにアクセスできるオブジェクトを生成する
  2. フレーム数を取得
    • CGImageSourceGetCount でアニメーション GIF に含まれるフレーム数を調べる
  3. ループ回数を読み取り
    • CGImageSourceCopyProperties → kCGImagePropertyGIFLoopCount からアニメーション GIF に設定されたループ回数を取得する
    • ただし UIImage.animatedImage では無限ループ固定のため実際の再生には反映されない
  4. フレームごとに UIImage を生成
    • CGImageSourceCreateImageAtIndex で各フレームを CGImage として取得し、UIImage に変換して images 配列へ追加する
  5. フレームの表示時間を加算
    • CGImageSourceCopyPropertiesAtIndex → kCGImagePropertyGIFDelayTime からフレームごとの表示時間(delayTime)を取得
    • それらを合計して duration(総再生時間)を計算する
  6. 再生時間のフォールバック処理
    • フレームごとの表示時間が取得できない場合は、1フレーム0.1秒として再生時間を設定する
  7. アニメーションUIImageを生成
    • UIImage.animatedImage(with: images, duration: duration) を呼び出して、無限ループ再生可能な UIImage を返す

SwiftUI側での使い方

GIFView(fileName: "sample.gif").frame(width: 300, height: 300)
  • GIF ファイルはメインバンドル内に配置しております

完成イメージ

まとめ

  • SwiftUI だけでは GIF アニメーションを再生できない
  • UIViewRepresentable で UIKit の UIImageView をラップすることで再生可能
  • アニメーション GIF データをフレーム単位に分解し、UIImage.animatedImage でアニメーションを生成する

おわりに

今回は、SwiftUI で GIF ファイルをアニメーション表示する方法について、UIViewRepresentable を使ったラッパーの作り方や、フレーム分解によるアニメーションの仕組みまで解説しました。

Swift や SwiftUI ではアニメーション GIF がそのまま動作しないため、ちょっとした工夫が必要ですが、今回の手法を使えば簡単に動く GIF ファイルを表示できます。

少しマニアックな内容かもしれませんが、開発者の方の参考になれば嬉しいです。 ぜひ、自分のプロジェクトに合わせてカスタマイズしてみてください。