
はじめに
こんにちは。プロダクト開発グループの于です。
今回は、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 { ... }
全体のフロー
- SwiftUI 側コンポーネント(GIFView)を用意
- UIViewRepresentable を使って UIKit のビューを SwiftUI に橋渡しする
 
- makeUIView で UI を作成
- コンテナ UIView を作成し、その中に UIImageView を追加して Auto Layout でコンテナにフィットさせる
 
- アニメーション GIF 読み込み処理を起動
- makeUIView 内から loadGIF(into:) を呼び、指定されたファイル名でバンドル内の アニメーション GIF を探す
 
- アニメーション GIF ファイルをデータ化
- Bundle.main でファイルパスを取得し、Data(バイト列)として読み込む
 
- GIF データをアニメーションUIImageに変換(UIImage拡張に委譲)
- GIF を読み込み、UIImage のアニメーション画像として生成
 
- 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) } }
処理の流れ
- CGImageSource を作成
- CGImageSourceCreateWithData でアニメーション GIF データを解析し、フレームやプロパティにアクセスできるオブジェクトを生成する
 
- フレーム数を取得
- CGImageSourceGetCount でアニメーション GIF に含まれるフレーム数を調べる
 
- ループ回数を読み取り
- CGImageSourceCopyProperties → kCGImagePropertyGIFLoopCount からアニメーション GIF に設定されたループ回数を取得する
- ただし UIImage.animatedImage では無限ループ固定のため実際の再生には反映されない
 
- フレームごとに UIImage を生成
- CGImageSourceCreateImageAtIndex で各フレームを CGImage として取得し、UIImage に変換して images 配列へ追加する
 
- フレームの表示時間を加算
- CGImageSourceCopyPropertiesAtIndex → kCGImagePropertyGIFDelayTime からフレームごとの表示時間(delayTime)を取得
- それらを合計して duration(総再生時間)を計算する
 
- 再生時間のフォールバック処理
- フレームごとの表示時間が取得できない場合は、1フレーム0.1秒として再生時間を設定する
 
- アニメーション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 ファイルを表示できます。
少しマニアックな内容かもしれませんが、開発者の方の参考になれば嬉しいです。 ぜひ、自分のプロジェクトに合わせてカスタマイズしてみてください。