swiftui-gif/Sources/SwiftUIGIF/SwiftUIGIF.swift

205 lines
5.7 KiB
Swift

import SwiftUI
public struct GIFImage: UIViewRepresentable {
private let data: Data?
private let name: String?
public init(data: Data) {
self.data = data
self.name = nil
}
public init(name: String) {
self.data = nil
self.name = name
}
public func makeUIView(context: Context) -> UIGIFImage {
if let data = data {
return UIGIFImage(data: data)
} else {
return UIGIFImage(name: name ?? "")
}
}
public func updateUIView(_ uiView: UIGIFImage, context: Context) {
if let data = data {
uiView.updateGIF(data: data)
} else {
uiView.updateGIF(name: name ?? "")
}
}
}
public class UIGIFImage: UIView {
private let imageView = UIImageView()
private var data: Data?
private var name: String?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
convenience init(name: String) {
self.init()
self.name = name
initView()
}
convenience init(data: Data) {
self.init()
self.data = data
initView()
}
public override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = bounds
self.addSubview(imageView)
}
func updateGIF(data: Data) {
updateWithImage {
UIImage.gifImage(data: data)
}
}
func updateGIF(name: String) {
updateWithImage {
UIImage.gifImage(name: name)
}
}
private func updateWithImage(_ getImage: @escaping () -> UIImage?) {
DispatchQueue.global(qos: .userInteractive).async {
let image = getImage()
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
private func initView() {
imageView.contentMode = .scaleAspectFit
}
}
public extension UIImage {
class func gifImage(data: Data) -> UIImage? {
guard let source = CGImageSourceCreateWithData(data as CFData, nil)
else {
return nil
}
let count = CGImageSourceGetCount(source)
let delays = (0..<count).map {
// store in ms and truncate to compute GCD more easily
Int(delayForImage(at: $0, source: source) * 1000)
}
let duration = delays.reduce(0, +)
let gcd = delays.reduce(0, gcd)
var frames = [UIImage]()
for i in 0..<count {
if let cgImage = CGImageSourceCreateImageAtIndex(source, i, nil) {
let frame = UIImage(cgImage: cgImage)
let frameCount = delays[i] / gcd
for _ in 0..<frameCount {
frames.append(frame)
}
} else {
return nil
}
}
return UIImage.animatedImage(with: frames,
duration: Double(duration) / 1000.0)
}
class func gifImage(name: String) -> UIImage? {
guard let url = Bundle.main.url(forResource: name, withExtension: "gif"),
let data = try? Data(contentsOf: url)
else {
return nil
}
return gifImage(data: data)
}
}
private func gcd(_ a: Int, _ b: Int) -> Int {
let absB = abs(b)
let r = abs(a) % absB
if r != 0 {
return gcd(absB, r)
} else {
return absB
}
}
private func delayForImage(at index: Int, source: CGImageSource) -> Double {
let defaultDelay = 1.0
let cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil)
let gifPropertiesPointer = UnsafeMutablePointer<UnsafeRawPointer?>.allocate(capacity: 0)
defer {
gifPropertiesPointer.deallocate()
}
let unsafePointer = Unmanaged.passUnretained(kCGImagePropertyGIFDictionary).toOpaque()
if CFDictionaryGetValueIfPresent(cfProperties, unsafePointer, gifPropertiesPointer) == false {
return defaultDelay
}
let gifProperties = unsafeBitCast(gifPropertiesPointer.pointee, to: CFDictionary.self)
var delayWrapper = unsafeBitCast(CFDictionaryGetValue(gifProperties,
Unmanaged.passUnretained(kCGImagePropertyGIFUnclampedDelayTime).toOpaque()),
to: AnyObject.self)
if delayWrapper.doubleValue == 0 {
delayWrapper = unsafeBitCast(CFDictionaryGetValue(gifProperties,
Unmanaged.passUnretained(kCGImagePropertyGIFDelayTime).toOpaque()),
to: AnyObject.self)
}
if let delay = delayWrapper as? Double,
delay > 0 {
return delay
} else {
return defaultDelay
}
}
struct GIFImageTest: View {
@State private var imageData: Data? = nil
var body: some View {
VStack {
GIFImage(name: "preview")
.frame(height: 300)
if let data = imageData {
GIFImage(data: data)
.frame(width: 300)
} else {
Text("Loading...")
.onAppear(perform: loadData)
}
}
}
private func loadData() {
let task = URLSession.shared.dataTask(with: URL(string: "https://github.com/globulus/swiftui-webview/raw/main/Images/preview_macos.gif?raw=true")!) { data, response, error in
imageData = data
}
task.resume()
}
}
struct GIFImage_Previews: PreviewProvider {
static var previews: some View {
GIFImageTest()
}
}