import SwiftUI // 1. Use looped H/VStacks to create a grid // 2. Conditionally increase spacing to grow/shrink the grid // 3. Calculate the distance of each dot to the center and use the value to stagger the animation //4. Add random delay on top of the staggered delay value struct ContentView: View { // const & state var dotSize: CGFloat = 4 var gridWidth: Int = 24 var gridHeight: Int = 32 var defaultSpacing: CGFloat = 6 var targetSpacing: CGFloat = 10 @State var animated: Bool = false // computed var spacing: CGFloat { return animated ? targetSpacing : defaultSpacing } // fn // play around with the t argument to speed up or slow down the effect func delay(t: Double, row: Int, col: Int) -> Double { let centerX = gridWidth / 2 let centerY = gridHeight / 2 let distance = sqrt(Double((row - centerY) * (row - centerY) + (col - centerX) * (col - centerX))) return (distance * Double.random(in: 0.1...0.3)) * t } var body: some View { ZStack { Color.black VStack(spacing: spacing) { ForEach(0..<gridHeight, id: \.self) { i in HStack(spacing: spacing) { ForEach(0..<gridWidth, id: \.self) { j in Ellipse() .fill(.white) .frame(width: dotSize, height: dotSize) .opacity(animated ? 0.75 : 0.5) .animation( .interactiveSpring( response: 0.5, dampingFraction: animated ? 0.5 : 0.5) .delay(delay(t: animated ? 0.35 : -10.0, row: i, col: j)), value: animated ) } } } } } .gesture( // use DragGesture as a hacky "while touch down" DragGesture(minimumDistance: 0) .onChanged { _ in animated = true } .onEnded { _ in animated = false } ) .ignoresSafeArea() } } #Preview { ContentView2() }