突然發現SwiftUI的Image貌似不支持播放GIF,那就只能自己嘗試實現一把。
Demo:Github地址
實現方案
1. SwiftUI中使用UIKit - UIViewRepresentable
SwiftUI
的Image
和AsyncImage
目前發現并不支持播放GIF,既然如此,最簡單的實現就是將UIKit
的UIImageView
應用到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) -> MyView
和func 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加載方式(
SDWebImage
、KingFisher
)應該會更好,本文只是介紹實現方案,所以用最簡單的方式實現。
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地址