Skip to content

Instantly share code, notes, and snippets.

@LidorFadida
Created December 19, 2024 20:22
Show Gist options
  • Select an option

  • Save LidorFadida/8f51908fc20aadd2ced604b22ad945d7 to your computer and use it in GitHub Desktop.

Select an option

Save LidorFadida/8f51908fc20aadd2ced604b22ad945d7 to your computer and use it in GitHub Desktop.
//MARK: - MarqueeEffectView.swift
struct MarqueeEffectView<Content: View>: View {
private let cycleDuration: TimeInterval
@Binding var isPaused: Bool
private let animationCycleDidComplete: (() -> Void)?
private let content: Content
@State private var xOffset: CGFloat = 0.0
@State private var deltaDate = Date.now
init(
cycleDuration: TimeInterval = 3.0,
isPaused: Binding<Bool>,
@ViewBuilder content: () -> Content,
animationCycleDidComplete: (() -> Void)?
) {
self.cycleDuration = cycleDuration
self._isPaused = isPaused
self.content = content()
self.animationCycleDidComplete = animationCycleDidComplete
}
var body: some View {
TimelineView(.animation(paused: isPaused)) { context in
GeometryReader { proxy in
let width = proxy.size.width
let height = proxy.size.height
ZStack {
HStack(spacing: .zero) {
content
.frame(width: width, height: height)
content
.frame(width: width, height: height)
}
.offset(x: -xOffset)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onChange(of: isPaused) { _, isPaused in
let deltaDate: Date = isPaused ? .now : (.now - (xOffset / width) * cycleDuration)
self.deltaDate = deltaDate
}
.onChange(of: context.date) { _, newValue in
let elapsedTime = newValue.timeIntervalSince(deltaDate)
handle(elapsedTimeUpdate: elapsedTime, width: width)
}
}
}
}
func handle(elapsedTimeUpdate: TimeInterval, width: CGFloat) {
if elapsedTimeUpdate >= cycleDuration {
deltaDate = .now
animationCycleDidComplete?()
} else {
let progress = (elapsedTimeUpdate / cycleDuration).truncatingRemainder(dividingBy: 1.0)
let targetXOffset = (progress * width)
self.xOffset = targetXOffset
}
}
}
#Preview("Mountain Hiking Scene", traits: .landscapeLeft) {
@Previewable @State var isPaused: Bool = false
ZStack(alignment: .bottomLeading) {
MarqueeEffectView(cycleDuration: 10.0, isPaused: $isPaused) {
ZStack {
Image(.mountainsBackground)
.resizable()
}
} animationCycleDidComplete: {
//..
}
AnimatedImageView(images: UIImage.walkSequence, animationDuration: 1.0)
.frame(width: 100.0, height: 200.0)
.offset(x: 120.0)
}
.ignoresSafeArea()
}
//MARK: - AnimatedImageView.swift
struct AnimatedImageView: View {
let images: [UIImage]
let animationDuration: TimeInterval
var body: some View {
if images.isEmpty {
EmptyView()
} else {
TimelineView(.animation) { timeline in
let timeInterval = animationDuration / Double(images.count)
let currentTime = timeline.date.timeIntervalSinceReferenceDate
let index = Int(currentTime / timeInterval) % images.count
if let uiImage = images[safe: index] {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.transition(.opacity)
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment