Skip to content

Instantly share code, notes, and snippets.

@cyypherus
Created March 17, 2022 18:40
Show Gist options
  • Save cyypherus/a4302aff0ba79aae27ebd7d22e8a6115 to your computer and use it in GitHub Desktop.
Save cyypherus/a4302aff0ba79aae27ebd7d22e8a6115 to your computer and use it in GitHub Desktop.
import SwiftUI
import Foundation
import Combine
struct ContentView: View {
static let defaultIcons: [Icon] = [
.init(),
.init(),
.init(),
.init(),
.init(),
.init(),
.init(),
.init(),
.init(),
.init()
]
struct Icon: Identifiable {
let id = UUID()
}
@State var visibleIndices: [Array<Icon>.Index]
let icons: [Icon]
let rate: Double
var timer: Timer.TimerPublisher
// Caveat - if window size is big enough you'll see icons animate from the beginning to the end as the window rolls.
// The window needs to be small enough that the icons will be fully removed before they're re-added at the end
init(_ icons: [Icon] = Self.defaultIcons, rate: Double = 1, rollingWindowSize: Int = 7) {
self.icons = icons
// Insertion and removal is animated at the edges of the marquee - this rolls the window so we still see zero at the beginning, even though we shifted the HStack to hide removal
var visible = icons.indices[0..<rollingWindowSize].map { $0 }
visible.retreatWindow(icons.indices.map { $0 })
self.visibleIndices = visible
self.rate = rate
self.timer = Timer.publish(every: rate, on: .main, in: .default)
}
var body: some View {
HStack(spacing: 10) {
ForEach(visibleIndices, id: \.self) { i in
RoundedRectangle(cornerRadius: 21)
.frame(width: 95, height: 95)
.foregroundColor(.indigo)
.overlay {
Text("\(i)")
}
}
}
// SwiftUI will automatically .center align if things don't fit in their container.
// These two .frame modifiers trick swiftUI so it thinks it fits so that it will .leading align 😈
// Second frame modifier fills width - we don't actually want zero width
// This layout can be accomplished with a geometry reader as well
.frame(width: 0, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .leading)
// Insertion and removal is animated at the edges of the marquee - this hides it off screen
.offset(x: -105)
.onReceive(timer) { _ in
advanceMarquee()
}
.onAppear { // So that you don't have to wait a second for it to begin scrolling
advanceMarquee()
_ = timer.connect() // Start timer
}
}
func advanceMarquee() {
withAnimation(.linear(duration: rate)) {
visibleIndices.advanceWindow(icons.indices.map { $0 })
}
}
}
extension Array where Element == Int {
mutating func advanceWindow(_ source: [Int]) {
if let last = last,
source.indices.contains(last + 1) {
removeFirst()
append(last + 1)
} else if let first = source.first {
removeFirst()
append(first)
}
}
mutating func retreatWindow(_ source: [Int]) {
if let first = first,
source.indices.contains(first - 1) {
removeLast()
insert(first - 1, at: 0)
} else if let last = source.last {
removeLast()
insert(last, at: 0)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment