Last active
March 15, 2024 09:09
-
-
Save chriseidhof/6bb9c4c3b36a85196c1ef3544b82f0db to your computer and use it in GitHub Desktop.
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
// | |
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