Skip to content

Instantly share code, notes, and snippets.

@swiftui-lab
Last active August 16, 2024 06:03
Show Gist options
  • Save swiftui-lab/aa5d73b81c8696dee4a5996954b22e5c to your computer and use it in GitHub Desktop.
Save swiftui-lab/aa5d73b81c8696dee4a5996954b22e5c to your computer and use it in GitHub Desktop.
// Author: The SwiftUI-Lab
// This code is part of the tutorial: https://swiftui-lab.com/swiftui-animations-part4/
import SwiftUI
// Sample usage
struct ContentView: View {
var body: some View {
VStack {
GifImage(url: URL(string: "https://media.giphy.com/media/YAlhwn67KT76E/giphy.gif?cid=790b7611b26260b2ad23535a70e343e67443ff80ef623844&rid=giphy.gif&ct=g")!)
.padding(10)
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke(.green)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// ObservableObject that holds the data and logic to get all frames in the gif image.
class GifData: ObservableObject {
var loopCount: Int = 0
var width: CGFloat = 0
var height: CGFloat = 0
var capInsets: EdgeInsets?
var resizingMode: Image.ResizingMode
struct ImageFrame {
let image: Image
let delay: TimeInterval
}
var frames: [ImageFrame] = []
init(url: URL, capInsets: EdgeInsets?, resizingMode: Image.ResizingMode) {
self.capInsets = capInsets
self.resizingMode = resizingMode
let label = url.deletingPathExtension().lastPathComponent
Task {
guard let (data, _) = try? await URLSession.shared.data(from: url) else { return }
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return }
let imageCount = CGImageSourceGetCount(source)
guard let imgProperties = CGImageSourceCopyProperties(source, nil) as? Dictionary<String, Any> else { return }
guard let gifProperties = imgProperties[kCGImagePropertyGIFDictionary as String] as? Dictionary<String, Any> else { return }
loopCount = gifProperties[kCGImagePropertyGIFLoopCount as String] as? Int ?? 0
width = gifProperties[kCGImagePropertyGIFCanvasPixelWidth as String] as? CGFloat ?? 0
height = gifProperties[kCGImagePropertyGIFCanvasPixelHeight as String] as? CGFloat ?? 0
let frameInfo = gifProperties[kCGImagePropertyGIFFrameInfoArray as String] as? [Dictionary<String, TimeInterval>] ?? []
for i in 0 ..< min(imageCount, frameInfo.count) {
if let image = CGImageSourceCreateImageAtIndex(source, i, nil) {
var img = Image(image, scale: 1.0, label: Text(label))
if let insets = capInsets {
img = img.resizable(capInsets: insets, resizingMode: resizingMode)
}
frames.append(
ImageFrame(image: img,
delay: frameInfo[i][kCGImagePropertyGIFDelayTime as String] ?? 0.05)
)
}
}
DispatchQueue.main.async { self.objectWillChange.send() }
}
}
}
// The GifImage view
struct GifImage: View {
@StateObject var gifData: GifData
/// Create an animated Gif Image
/// - Parameters:
/// - url: the url holding the animated gif file
/// - capInsets: if nil, image is not resizable. Otherwise, the capInsets for image resizing (same as the standard image .resizable() modifier).
/// - resizingMode: ignored if capInsets is nil, otherwise, equivalent to the standard image .resizable() modifier parameter)
init(url: URL, capInsets: EdgeInsets? = nil, resizingMode: Image.ResizingMode = .stretch) {
_gifData = StateObject(wrappedValue: GifData(url: url, capInsets: capInsets, resizingMode: resizingMode))
}
var body: some View {
Group {
if gifData.frames.count == 0 {
Color.clear
} else {
VStack {
TimelineView(.cyclic(loopCount: gifData.loopCount, timeOffsets: gifData.frames.map { $0.delay })) { timeline in
ImageFrame(gifData: gifData, date: timeline.date)
}
}
}
}
}
struct ImageFrame: View {
@State private var frame = 0
let gifData: GifData
let date: Date
var body: some View {
gifData.frames[frame].image
.onChange(of: date) { _ in
frame = (frame + 1) % gifData.frames.count
}
}
}
}
// A cyclic TimelineSchedule
struct CyclicTimelineSchedule: TimelineSchedule {
let loopCount: Int // loopCount == 0 means inifinite loops.
let timeOffsets: [TimeInterval]
func entries(from startDate: Date, mode: TimelineScheduleMode) -> Entries {
Entries(loopCount: loopCount, last: startDate, offsets: timeOffsets)
}
struct Entries: Sequence, IteratorProtocol {
let loopCount: Int
var loops = 0
var last: Date
let offsets: [TimeInterval]
var idx: Int = -1
mutating func next() -> Date? {
idx = (idx + 1) % offsets.count
if idx == 0 { loops += 1 }
if loopCount != 0 && loops >= loopCount { return nil }
last = last.addingTimeInterval(offsets[idx])
return last
}
}
}
extension TimelineSchedule where Self == CyclicTimelineSchedule {
static func cyclic(loopCount: Int, timeOffsets: [TimeInterval]) -> CyclicTimelineSchedule {
.init(loopCount: loopCount, timeOffsets: timeOffsets)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment