Created
December 19, 2024 20:22
-
-
Save LidorFadida/8f51908fc20aadd2ced604b22ad945d7 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| //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