Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Last active March 15, 2024 09:09
Show Gist options
  • Save chriseidhof/6bb9c4c3b36a85196c1ef3544b82f0db to your computer and use it in GitHub Desktop.
Save chriseidhof/6bb9c4c3b36a85196c1ef3544b82f0db to your computer and use it in GitHub Desktop.
//
import SwiftUI
struct Helper<Result: View>: _VariadicView_MultiViewRoot {
var _body: (_VariadicView.Children) -> Result
func body(children: _VariadicView.Children) -> some View {
_body(children)
}
}
extension View {
func variadic<R: View>(@ViewBuilder process: @escaping (_VariadicView.Children) -> R) -> some View {
_VariadicView.Tree(Helper(_body: process), content: { self })
}
}
struct ContentView: View {
var body: some View {
MarqueeLine {
ForEach(0..<10) { ix in
Text("Child \(ix)")
.padding(3)
.padding(.horizontal)
.background {
Capsule()
.fill(Color.accentColor)
}
}
}
}
}
struct Measured: Hashable {
var start0: CGFloat? = nil // minX of the first set of children
var start1: CGFloat? = nil // minX of the second set of children
init() { }
init(_ value: CGFloat, for keyPath: WritableKeyPath<Self, CGFloat?>) {
self[keyPath: keyPath] = value
}
}
struct MyPref: PreferenceKey {
static let defaultValue = Measured()
static func reduce(value: inout Measured, nextValue: () -> Measured) {
let n = nextValue()
value.start0 = value.start0 ?? n.start0
value.start1 = value.start1 ?? n.start1
}
}
extension View {
func measureMinX(for keyPath: WritableKeyPath<Measured, CGFloat?>) -> some View {
overlay {
GeometryReader { proxy in
// let _ = print(proxy.frame(in: .named("stack")))
let result = Measured(proxy.frame(in: .named("stack")).minX, for: keyPath)
Color.clear.preference(key: MyPref.self, value: result)
}
}
}
}
struct MarqueeLine<Children: View>: View {
@ViewBuilder var children: Children
@State private var offset: CGFloat = 0
@State private var childrenWidth: CGFloat = 0
var body: some View {
HStack {
HStack {
children
}.measureMinX(for: \.start0)
HStack {
children
}.measureMinX(for: \.start1)
children
}
.coordinateSpace(.named("stack"))
// repeating children three times is a hack, of course. doesn't cover all the edge cases. could be made smarter
.fixedSize(horizontal: true, vertical: false) // make sure each subview becomes its ideal size in the horizontal direction
.padding() // add some padding to the animated content
.offset(x: offset) // this is what we animate
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) // become the proposed width, but leading-align the content
.onPreferenceChange(MyPref.self, perform: { value in
let value = value.start1! - value.start0! // todo force unwrap
if value != childrenWidth {
childrenWidth = value
print("Children width", childrenWidth)
}
})
.onAppear {
startAnimation()
}
}
// The idea here is that we animate all the way until we get to the minX of the repeated children and then quickly set to zero before animating again
func startAnimation() {
let distance: CGFloat = childrenWidth
let speed: CGFloat = 50
withAnimation(.linear(duration: distance/speed)) {
offset = -distance
} completion: {
offset = 0
startAnimation()
}
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment