Last active
July 1, 2024 15:27
-
-
Save unuigbee/a12ed61a84d862ad5462f796c9c42e42 to your computer and use it in GitHub Desktop.
Looping GLTransitions in SwiftUI using MagicKit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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