Skip to content

Instantly share code, notes, and snippets.

@unuigbee
Last active July 1, 2024 15:27
Show Gist options
  • Save unuigbee/a12ed61a84d862ad5462f796c9c42e42 to your computer and use it in GitHub Desktop.
Save unuigbee/a12ed61a84d862ad5462f796c9c42e42 to your computer and use it in GitHub Desktop.
Looping GLTransitions in SwiftUI using MagicKit
// https://github.com/mrcreatoor/MagicKit
import MagicKit
import SwiftUI
import Combine
// MARK: Implementation
public struct LoopingAnimationView<Content: View>: View {
private let animatableViews: [Content]
@State private var timer: Timer.TimerPublisher
@State private var timerCancellable: Cancellable?
@State var show = false
@State private var currentIndex = 1
private var nextItemIndex: Int {
let next = (currentIndex + 1) % animatableViews.count
return next
}
private var shouldAnimate: Bool { animatableViews.count > 1 }
public init(animatableViews: [Content]) {
self.animatableViews = animatableViews
_timer = .init(
initialValue: Timer.publish(
every: 3.0,
on: .main,
in: .common
)
)
}
public var body: some View {
ZStack {
ForEach(Array(animatableViews.enumerated()), id: \.offset) { index, _ in
if currentIndex == index {
renderTransitioningViews(at: index)
}
}
}
.onAppear(perform: restartTimer)
.onDisappear(perform: invalidateTimer)
.onReceive(timer) { _ in currentIndex = nextItemIndex }
}
func renderTransitioningViews(at index: Int) -> some View {
renderOutgoingView(at: index)
.magic(
transition: .bottomTop,
duration: 1.0,
show: $show,
{ renderIncomingView(at: index) }
)
}
private func renderOutgoingView(at index: Int) -> some View {
let fromIndex = index - 1
return animatableViews[
// ensures we are looping through the array of animatable views
((fromIndex % animatableViews.count) + animatableViews.count) % animatableViews.count
]
.onAppear {
DispatchQueue.main.async {
show = true
}
}
}
private func renderIncomingView(at index: Int) -> some View {
return animatableViews[
((index % animatableViews.count) + animatableViews.count) % animatableViews.count
]
.onDisappear { show = false }
}
// MARK: Helpers
private func restartTimer() {
guard shouldAnimate else { return }
timerCancellable?.cancel()
timer = Timer.publish(
every: 3.0,
on: .main,
in: .common
)
timerCancellable = timer.connect()
}
private func invalidateTimer() {
guard shouldAnimate else { return }
timerCancellable?.cancel()
}
}
// MARK: Usage
struct Content: View {
var image1: UIImage = {
let image = UIImage(named: "sample-splash1")!
return image
}()
var image2: UIImage = {
let image = UIImage(named: "sample-splash2")!
return .init(
cgImage: image.cgImage!,
scale: image.scale,
orientation: .downMirrored
)
}()
var image3: UIImage = {
let image = UIImage(named: "sample-splash3")!
return .init(
cgImage: image.cgImage!,
scale: image.scale,
orientation: .downMirrored
)
}()
var image4: UIImage = {
let image = UIImage(named: "sample-splash4")!
return image
}()
var body: some View {
LoopingAnimationView(
animatableViews: [
// first and last image orientation should not be .downMirrored
Image(uiImage: image1),
Image(uiImage: image2),
Image(uiImage: image3),
Image(uiImage: image4)
]
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment