SwiftUI 播放GIF的实现方案

突然发现SwiftUI的Image貌似不支持播放GIF,那就只能自己尝试实现一把。

Demo:Github地址

实现方案

1. SwiftUI中使用UIKit - UIViewRepresentable

SwiftUIImageAsyncImage目前发现并不支持播放GIF,既然如此,最简单的实现就是将UIKitUIImageView应用到SwiftUI中。

SwiftUI中使用UIKit控件,就得用到UIViewRepresentable协议去实现了:

import SwiftUI
import UIKit

struct GifImage: UIViewRepresentable {
    // GIF模型
    var resource: GifResource?
    // UIKit的内容显示模式
    var contentMode: UIView.ContentMode = .scaleAspectFill
    // 用于控制GIF的播放/暂停
    @Binding var isAnimating: Bool
    
    func makeUIView(context: Context) -> MyView { MyView() }
    
    func updateUIView(_ uiView: MyView, context: Context) {
        uiView.contentMode = contentMode
        uiView.updateGifResource(resource, isAnimating)
    }
    
    ......
}
  • GifResource是提供GIF的图片、时长的模型类;
  • func makeUIView(context: Context) -> MyViewfunc updateUIView(_ uiView: MyView, context: Context)UIViewRepresentable协议必须实现的两个函数,前者是创建你想用的UIView,后者是用来刷新该UIView,系统自己会调用,例如给resource属性赋值就会调起,所以我们应该在这个函数中设置内容。

其中MyView是我自己自定义的一个UIView,上面放着一个UIImageView专门播放GIF:

class MyView: UIView {
    private let imageView = UIImageView()
    private var resource: GifResource?
        
    init() {
        super.init(frame: .zero)
        clipsToBounds = true
        
        imageView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(imageView)
        NSLayoutConstraint.activate([
            imageView.widthAnchor.constraint(equalTo: widthAnchor),
            imageView.heightAnchor.constraint(equalTo: heightAnchor),
        ])
    }
        
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
        
    override var contentMode: UIView.ContentMode {
        set { imageView.contentMode = newValue }
        get { imageView.contentMode }
    }
        
    ......
}
  • 为什么不直接使用UIImageView?如果直接使用UIImageView,整个视图的尺寸在SwiftUI将不受控制(图片多大视图就多大),这个目前我也不知道为什么,但神奇的是在其上面放入UIImageView并添加约束即可限制大小。
  • 其实这里使用第三方的GIF加载方式(SDWebImageKingFisher)应该会更好,本文只是介绍实现方案,所以用最简单的方式实现。

2. 解码GIF文件

GIF的解码过程我写在了UIImage的分类中,并且使用了async/await适配SwiftUI,方便调起:

import UIKit

extension UIImage {
    static func decodeGif(fromBundle name: String) async throws -> GifResource {
        ......
    }
    
    static func decodeGif(withUrl url: URL?) async throws -> GifResource {
        ......
    }
    
    static func decodeGif(withData data: Data) async throws -> GifResource {
        ......
    }
}
  • 具体实现可以查看Demo,都是参考YYKit的做法然后“翻译”成Swift语言(Maybe会有问题,目前还没发现任何问题,凑合着用)。

3. 用起来

struct ContentView: View {
    @State var resource: GifResource? = nil
    
    var body: some View {
        VStack {
            GifImage(resource: resource, 
                     contentMode: .scaleAspectFit, 
                     isAnimating: .constant(true))
                .frame(width: 150, height: 150)
                .background(.ultraThinMaterial)
                .mask(RoundedRectangle(cornerRadius: 10, style: .continuous))
                .shadow(color: .black.opacity(0.3), radius: 10, x: 0, y: 10)
        }
        .task {
            resource = try? await UIImage.decodeGif(fromBundle: "Cat2")
        }
    }
}

4. AsyncGifImage - 异步加载远程/本地GIF

基于GifImage的扩展,一个可异步加载远程/本地GIF的View

/// 仿照`AsyncImage`
AsyncGifImage(url: url,
              contentMode: .scaleAspectFit,
              transaction: Transaction(animation: .easeInOut),
              isAnimating: $isAnimating,
              isReLoad: $isReload) { phase in
    switch phase {
        // 请求中
        case .loading: ProgressView()
        // 请求成功
        case let .success(image): image // image为GifImage
        // 请求失败
        case .failure: Text("Failure").font(.body.weight(.bold))
    }
}
  • 让使用者根据phase返回不同状态,自定义去提供不同时期的View
  • transaction:根据phase切换不同的View的过渡效果;
  • isAnimating:控制GIF的播放/暂停;
  • isReload:重载GIF。

最终效果

effect.gif

OK, done.

Demo:Github地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容