この記事について
筆者がかつて、或るiPhone向けアプリの開発中に画面の一部のページにおいてGIFアニメーションを表示してほしいという要望があり、その対応を実施した事があるのですが、その最中に膨大な量のメモリを使用して最悪の場合アプリがクラッシュしてしまう現象に遭遇し、調べた結果GIFアニメーションを表示するための外部ライブラリが原因であった事が分かり、選定を行った結果SwiftyGifを使用することになりました。
今回は改めて、現在iOSのGIFアニメーション表示で使えるライブラリの一覧を調べて記事にしました。
(簡単な)手法
実験するiPhoneアプリケーションは、2ページ構成とします。
- 1ページ目にはサイズが小さめのアニメーション(8フレーム、56KB)と、2ページ目に進むボタンを表示
- 2ページ目にはサイズがやや大きめのアニメーション(60フレーム、104KB)を表示
- それぞれのアニメーションは回数無制限でループするように指定する
(なお、今回の実験では著作権フリーのローディング用GIFアニメーションを使っています: http://wordpress.ideacompo.com/?p=4666)
比較対象
なお、ここで挙げているどのライブラリも、CocoapodsあるいはCarthageでの取得が可能ですので、ご自身の使用しているライブラリ管理に合わせて導入できます(また、蛇足ではありますが、一番単純な実装例もスニペットとして掲載しています)。各ライブラリのversionは、執筆時点(2023年2月)にてCocoapodsによってinstallできた最新版を記載しています。
Storyboardを使う場合(1): SwiftGifOrigin (ver. 1.7.0, unmaintained)
Swiftで利用できるGIFアニメーション表示ライブラリとしては SwiftGifOrigin が著名です。しかし、残念ながら2021年1月15日時点でリポジトリが凍結、実質の開発中止状態になっています。
同ライブラリにはメモリリークの問題1があり、かつて筆者もこのライブラリを使用して開発を続けていたところ、1枚が2MB程度のGIFアニメーションを表示しただけで、メモリ使用量が300〜400MB程度まで跳ね上がった事があったため、別のライブラリの選定を余儀なくされた経緯があります。
そこで実際に、SwiftGifOriginを使うとどれぐらい使用量が跳ね上がるのかを調査してみます。1枚目の例はこのような感じで作成します。導入するだけであればコードは下記のようになります。
import UIKit
import SwiftGifOrigin
class FirstViewController: UIViewController {
@IBOutlet weak var nextButton: UIButton!
@IBOutlet weak var animationGif: UIImageView!
override func viewWillAppear(_ animated: Bool) {
self.animationGif.loadGif(name: "sample1")
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
// SecondViewControllerの実装は以後割愛
class SecondViewController: UIViewController {
@IBOutlet weak var animationGif: UIImageView!
override func viewWillAppear(_ animated: Bool) {
self.animationGif.loadGif(name: "sample2")
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
1枚目の表示の時点では17MB程度の使用量でしたが、2枚目に遷移した時点で100MBを超えてしまいました。
Storyboardを使う場合(2): Gifu (ver. 3.3.1)
SwiftGifOriginが現時点では開発中止状態となっており、同ライブラリで乗り換え先として推奨されているのが Gifu です(今回の調査で初めて知りました)。GIFImageViewというモジュールのカスタムクラスを使用してStoryboardに指定する方法も可能ですし、GIFAnimatableクラスを拡張したカスタムクラスやUIImageViewを実装する方法についてもREADME内Usageの項に記載されています(当記事では単純な実験結果の確認のため、前者の方法を採っています)。
import UIKit
import Gifu
class FirstViewController: UIViewController {
@IBOutlet weak var nextButton: UIButton!
@IBOutlet weak var animationGif: GIFImageView!
override func viewWillAppear(_ animated: Bool) {
animationGif.animate(withGIFNamed: "sample1")
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
2枚目に遷移した時点でのメモリ使用量は24.5MB前後と、大分少なくなりました。README内のHow It Worksの項でも説明されていますが、Gifuでは読み込むフレーム数を節約する工夫が施されていることによって、パフォーマンスにも配慮したGIFアニメーションを表示する事ができるようです。
Storyboardを使う場合(3): SwiftyGif (ver. 5.4.3)
そして、今回の表題にもあるSwiftyGifです。
このライブラリはパフォーマンス面についても配慮されており、CPU負荷を上げる代わりにメモリ使用量を制限するような制御ができたり、integrity levelを下げてフレームドロップの頻度を上げる代わりに、複数のGIF画像をスライドショー式に表示するような場面でも表示時のパフォーマンスを優先することを選ぶこともできます。
import UIKit
import SwiftyGif
class FirstViewController: UIViewController {
@IBOutlet weak var nextButton: UIButton!
@IBOutlet weak var animationGif: UIImageView!
override func viewWillAppear(_ animated: Bool) {
let gifImage = try! UIImage(gifName: "sample1.gif") // 拡張子も必要
self.animationGif.setGifImage(gifImage, loopCount: -1)
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
2枚目に遷移した時点でのメモリ使用量が17.3MB前後と、1枚目に遷移した時点と大差なくなりました。
筆者が関わった案件では、特に複数のページでGIFアニメーションを頻繁に使っていたため、大きな改善効果が得られました。アプリ内で使用するGIFの枚数が多く、パフォーマンスに影響が出かねない状況においては、SwiftyGifを使うのがお勧めです。
SwiftUIを使う場合(1): SSSwiftUIGIFView (ver. 1.0.0)
さて、SwiftUIでのiPhoneアプリについては開発未経験のため改めて今回調査してみましたが、同様のライブラリがないかを探したところ、 SSSwiftUIGIFView が見付かりました。
import SwiftUI
import SSSwiftUIGIFView
struct ContentView: View {
@State var isActive: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: NextView(), isActive: $isActive) {
EmptyView()
}
Button("Next Page", action: { isActive = true})
SwiftUIGIFPlayerView(gifName: "sample1")
}
}
}
}
struct NextView: View {
var body: some View {
VStack {
SwiftUIGIFPlayerView(gifName: "sample2")
}
}
}
このようにSwiftUIのViewとしてSwiftUIGIFPlayerViewが定義されていますので、コードは非常に簡単に書く事ができます。
しかしながら、2枚目のGIFアニメーションを読み込んだ時点でメモリ使用量はSwiftGifOriginと同じように100MB超となってしまいました。また、2枚目のページからBackで戻っても、使用量は元には戻りませんでした。
SwiftUIを使う場合(2): SwiftyGif
SwiftUIでSwiftyGifを使う場合にはちょっとした注意が必要で、SwiftyGifで表示したいViewをUIRepresentableとして定義するコードも必要です。また、GitHubにもSwiftyGif in SwiftUIというイシューが寄せられており、表示されるGIFのサイズがframeなどで正しく指定できず横幅いっぱいに広がってしまうという問題があるようです。幸い、この方法は解決策が掲載されているため、筆者もそれにあやかり、同様の方法で対応することにしてみました。
import SwiftUI
import SwiftyGif
struct ContentView: View {
@State var playGif = true // アニメーションを止めるならfalse、Stateで切り替えも可能
@State var isActive: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: NextView(), isActive: $isActive) {
EmptyView()
}
Button("Next Page", action: { isActive = true})
SwiftyGif(name: "sample1.gif", loopCount: -1, playGif: $playGif)
.frame(width: 200, height: 200, alignment: .center)
}
}
}
}
struct NextView: View {
@State var playGif = true
var gif = UIImageView()
var body: some View {
VStack {
SwiftyGif(name: "sample2.gif", loopCount: -1, playGif: $playGif)
.frame(width: 200, height: 200, alignment: .center)
}
}
}
class UIGIFImageView: UIView {
private var image = UIImage()
var imageView = UIImageView()
private var data: Data?
private var name: String?
private var loopCount: Int?
open var playGif: Bool?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
convenience init(name: String, loopCount: Int, playGif: Bool) {
self.init()
self.name = name
self.loopCount = loopCount
self.playGif = playGif
createGIF()
}
convenience init(data: Data, loopCount: Int, playGif: Bool) {
self.init()
self.data = data
self.loopCount = loopCount
self.playGif = playGif
createGIF()
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = bounds
self.addSubview(imageView)
}
func createGIF() {
do {
if let data = data {
image = try UIImage(gifData: data)
} else {
image = try UIImage(gifName: self.name!)
}
} catch {
print(error)
}
imageView = UIImageView(gifImage: image, loopCount: loopCount!)
// .scaleAspectFit keeps the aspect ratio of the gif
imageView.contentMode = .scaleAspectFit
}
}
struct SwiftyGif: UIViewRepresentable {
private let data: Data?
private let name: String?
private let loopCount: Int?
@Binding var playGif: Bool
init(data: Data, loopCount: Int = -1, playGif: Binding<Bool> = .constant(true)) {
self.data = data
self.name = nil
self.loopCount = loopCount
self._playGif = playGif
}
init(name: String, loopCount: Int = -1, playGif: Binding<Bool> = .constant(true)) {
self.data = nil
self.name = name
self.loopCount = loopCount
self._playGif = playGif
}
func makeUIView(context: Context) -> UIGIFImageView {
var gifImageView: UIGIFImageView
if let data = data {
gifImageView = UIGIFImageView(data: data, loopCount: loopCount!, playGif: playGif)
} else {
gifImageView = UIGIFImageView(name: name!, loopCount: loopCount!, playGif: playGif)
}
return gifImageView
}
func updateUIView(_ gifImageView: UIGIFImageView, context: Context) {
if playGif {
gifImageView.imageView.startAnimatingGif()
} else {
gifImageView.imageView.stopAnimatingGif()
}
}
}
実際にメモリ使用量を見てみると、やはりStoryboard版のSwiftyGifを使った時と同様にメモリ使用量が大幅に抑えられています。
まとめ
まとめると以下のようになります:
- ライブラリによってはGIFアニメーションを読み込んだ時のメモリ使用量が跳ね上がる事がある
- SwiftyGifを利用したほうがGIFアニメーション表示にかかるメモリ使用量が低下する(特に複数のアニメーションを使う場合に顕著)
- ただし、SwiftUIでSwiftyGifを使う際にはそれ向けの実装のほか、イシュー対策が必要
- Gifuも悪くはない選択肢である(なおSwiftUIで利用するときには、おそらくSwiftyGifをSwiftUIで使用するようなコーディングが必要と考えられるが、本記事ではその検証はしていない)。
また、iOSのGIFアニメーション表示については、@ushisantoasobu さんの記事(2017年のmixiグループ Advent Calendar向け記事)もあり、そちらはSwiftyGifやGifuの内部実装についても解説されています。
-
簡単に検索しただけでも、 Swiftでgifが実機で再生されない (Teratail), Memory leak issue - swiftgif/SwiftGif (GitHubのissue) のような記事を発見した。 ↩