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閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容