Skip to content

Commit 4fca907

Browse files
committed
Initial implementation of Stagger
1 parent 071e2b5 commit 4fca907

16 files changed

+1289
-3
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ playground.xcworkspace
2727
#
2828
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
2929
# hence it is not needed unless you have added a package configuration file to your project
30-
# .swiftpm
30+
.swiftpm
3131

3232
.build/
3333

@@ -59,4 +59,4 @@ Carthage/Build/
5959
fastlane/report.xml
6060
fastlane/Preview.html
6161
fastlane/screenshots/**/*.png
62-
fastlane/test_output
62+
fastlane/test_output

.swiftlint.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
included:
2+
- Sources
3+
- Tests
4+
excluded:
5+
- Tests/LinuxMain.swift
6+
- .build
7+
- Examples
8+
9+
opt_in_rules:
10+
- empty_count
11+
- empty_string
12+
- conditional_returns_on_newline
13+
- closure_spacing
14+
- contains_over_first_not_nil
15+
- discouraged_optional_boolean
16+
- explicit_init
17+
- fatal_error_message
18+
- first_where
19+
- force_unwrapping
20+
- implicit_return
21+
- implicitly_unwrapped_optional
22+
- joined_default_parameter
23+
- modifier_order
24+
- multiline_parameters
25+
- operator_usage_whitespace
26+
- overridden_super_call
27+
- prohibited_super_call
28+
- sorted_first_last
29+
- trailing_closure
30+
- unneeded_parentheses_in_closure_argument
31+
- vertical_parameter_alignment_on_call
32+
- yoda_condition
33+
34+
disabled_rules:
35+
- identifier_name
36+
- nesting
37+
- function_parameter_count
38+
39+
line_length:
40+
warning: 120
41+
error: 150
42+
ignores_comments: true
43+
44+
file_length:
45+
warning: 500
46+
error: 1200
47+
48+
type_body_length:
49+
warning: 400
50+
error: 600
51+
52+
function_body_length:
53+
warning: 50
54+
error: 100
55+
56+
large_tuple:
57+
warning: 3
58+
error: 5
59+
60+
cyclomatic_complexity:
61+
warning: 10
62+
error: 20
63+
64+
reporter: "xcode"

Package.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "Stagger",
7+
platforms: [
8+
.iOS(.v17),
9+
.macOS(.v14),
10+
.tvOS(.v17)
11+
],
12+
products: [
13+
.library(
14+
name: "Stagger",
15+
targets: ["Stagger"]
16+
)
17+
],
18+
targets: [
19+
.target(name: "Stagger"),
20+
.testTarget(
21+
name: "StaggerTests",
22+
dependencies: ["Stagger"]
23+
)
24+
]
25+
)

README.md

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,168 @@
1-
# swiftui-stagger-animation
1+
# Stagger
2+
3+
A SwiftUI library for creating beautiful staggered animations with minimal code.
4+
5+
This project is based on the [objc.io Swift Talk episode "Staggered Animations Revisited"](https://talk.objc.io/episodes/S01E443-staggered-animations-revisited).
6+
7+
[![Swift](https://img.shields.io/badge/Swift-6.0-orange.svg)](https://swift.org)
8+
[![iOS](https://img.shields.io/badge/iOS-17.0+-blue.svg)](https://developer.apple.com/ios)
9+
[![macOS](https://img.shields.io/badge/macOS-14.0+-blue.svg)](https://developer.apple.com/macos)
10+
[![tvOS](https://img.shields.io/badge/tvOS-17.0+-blue.svg)](https://developer.apple.com/tvos)
11+
[![MIT](https://img.shields.io/badge/license-MIT-black.svg)](https://opensource.org/licenses/MIT)
12+
13+
## Features
14+
15+
- 🌊 **Simple API**: Add staggered animations with just a single view modifier
16+
- 🔄 **Customizable**: Control animation timing, order, and transitions
17+
- 📱 **Accessibility**: Respects reduced motion settings
18+
- 🧩 **Composable**: Works with any SwiftUI transition
19+
- 🔍 **Smart sorting**: Order by position, priority, or custom criteria
20+
21+
## Installation
22+
23+
### Swift Package Manager
24+
25+
Add the following to your `Package.swift` file:
26+
27+
```swift
28+
dependencies: [
29+
.package(url: "https://github.com/ivan-magda/swiftui-stagger-animation.git", from: "1.0.0")
30+
]
31+
```
32+
33+
Or add it in Xcode:
34+
1. Go to File → Add Packages...
35+
2. Paste the repository URL: `https://github.com/ivan-magda/swiftui-stagger-animation.git`
36+
3. Click "Add Package"
37+
38+
## Usage
39+
40+
### Basic Usage
41+
42+
```swift
43+
VStack {
44+
ForEach(items) { item in
45+
ItemView(item: item)
46+
.stagger() // Default opacity transition
47+
}
48+
}
49+
.staggerContainer() // Required to coordinate animations
50+
```
51+
52+
### Custom Transitions
53+
54+
```swift
55+
// Single transition
56+
Text("Hello").stagger(transition: .move(edge: .leading))
57+
58+
// Combined transitions
59+
Image(systemName: "star")
60+
.stagger(transition: .scale.combined(with: .opacity))
61+
```
62+
63+
### Controlling Animation Order
64+
65+
```swift
66+
// Setting animation priority (higher values animate first)
67+
Text("First").stagger(priority: 10)
68+
Text("Second").stagger(priority: 5)
69+
Text("Third").stagger(priority: 0)
70+
71+
// Configure stagger container
72+
VStack {
73+
// Your views...
74+
}
75+
.staggerContainer(
76+
configuration: StaggerConfiguration(
77+
baseDelay: 0.1, // Time between each item
78+
animationCurve: .spring(response: 0.5),
79+
calculationStrategy: .priorityThenPosition(.topToBottom)
80+
)
81+
)
82+
```
83+
84+
### Animation Strategies
85+
86+
```swift
87+
// Available calculation strategies
88+
.priorityThenPosition(.leftToRight) // Default
89+
.priorityOnly
90+
.positionOnly(.topToBottom)
91+
.custom { lhs, rhs in
92+
// Your custom sorting logic
93+
}
94+
95+
// Available directions
96+
.leftToRight
97+
.rightToLeft
98+
.topToBottom
99+
.bottomToTop
100+
```
101+
102+
### Animation Curves
103+
104+
```swift
105+
// Available animation curves
106+
.default
107+
.easeIn
108+
.easeOut
109+
.easeInOut
110+
.spring(response: 0.5, dampingFraction: 0.8)
111+
.custom(Animation.interpolatingSpring(mass: 1, stiffness: 100, damping: 10))
112+
```
113+
114+
## Example
115+
116+
```swift
117+
struct ContentView: View {
118+
@State private var isVisible = false
119+
120+
let colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple]
121+
122+
var body: some View {
123+
VStack(spacing: 16) {
124+
Text("Stagger Animation Demo")
125+
.font(.largeTitle)
126+
.stagger(
127+
transition: .move(edge: .top).combined(with: .opacity),
128+
priority: 10
129+
)
130+
131+
LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))], spacing: 16) {
132+
ForEach(colors.indices, id: \.self) { index in
133+
RoundedRectangle(cornerRadius: 12)
134+
.fill(colors[index])
135+
.frame(height: 80)
136+
.stagger(transition: .scale.combined(with: .opacity))
137+
}
138+
}
139+
140+
Button("Reset") {
141+
isVisible.toggle()
142+
}
143+
.padding()
144+
}
145+
.padding()
146+
.staggerContainer(
147+
configuration: StaggerConfiguration(
148+
baseDelay: 0.08,
149+
animationCurve: .spring(response: 0.6)
150+
)
151+
)
152+
}
153+
}
154+
```
155+
156+
## Requirements
157+
158+
- iOS 17.0+ / macOS 14.0+ / tvOS 17.0+
159+
- Swift 6.0+
160+
- Xcode 15.0+
161+
162+
## Contributing
163+
164+
Contributions are welcome! Please feel free to submit a Pull Request.
165+
166+
## License
167+
168+
Stagger is available under the MIT license. See the LICENSE file for more info.

Sources/Stagger/Preview.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#if DEBUG
2+
import SwiftUI
3+
4+
struct MyColor: Identifiable {
5+
var id = UUID()
6+
var color: Color
7+
}
8+
9+
let sampleColors = (0..<10).map { ix in
10+
MyColor(color: .init(hue: .init(ix) / 20, saturation: 0.8, brightness: 0.8))
11+
}
12+
13+
@available(iOS 16.0, *)
14+
struct ContentView: View {
15+
@State private var colors = sampleColors
16+
17+
var body: some View {
18+
let rect = RoundedRectangle(cornerRadius: 16)
19+
VStack(spacing: 16) {
20+
rect.fill(.blue.gradient)
21+
.frame(height: 120)
22+
.stagger(
23+
transition: .move(edge: .top).combined(with: .opacity),
24+
priority: -1
25+
)
26+
27+
LazyVGrid(columns: [.init(.adaptive(minimum: 80), spacing: 16)], spacing: 16) {
28+
ForEach(colors) { color in
29+
rect.fill(color.color.gradient)
30+
.frame(height: 80)
31+
.stagger(transition: .scale.combined(with: .opacity))
32+
}
33+
}
34+
35+
Button("Add") {
36+
for _ in 0..<5 {
37+
colors.append(.init(color: Color(hue: .random(in: 0...1), saturation: 0.6, brightness: 0.6)))
38+
}
39+
}
40+
}
41+
.padding()
42+
.staggerContainer()
43+
}
44+
}
45+
46+
@available(iOS 16.0, *)
47+
#Preview {
48+
ContentView()
49+
}
50+
#endif

Sources/Stagger/Stagger+View.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import SwiftUI
2+
3+
extension View {
4+
/// Applies a staggered fade-in animation to this view.
5+
///
6+
/// Views modified with `stagger()` will animate in sequence when contained
7+
/// within a view that has the `staggerContainer()` modifier.
8+
///
9+
/// Example:
10+
/// ```swift
11+
/// Text("Hello, World!")
12+
/// .stagger(priority: 1)
13+
/// ```
14+
///
15+
/// - Parameter priority: Animation priority (higher values animate first). Default is 0.
16+
/// - Returns: A view with staggered animation applied.
17+
public func stagger(priority: Double = 0) -> some View {
18+
modifier(StaggerViewModifier(transition: .opacity, priority: priority))
19+
}
20+
21+
/// Applies a staggered animation with a custom transition to this view.
22+
///
23+
/// This allows for more complex animations like sliding, scaling, or rotating.
24+
///
25+
/// Example:
26+
/// ```swift
27+
/// Text("Hello, World!")
28+
/// .stagger(transition: .move(edge: .bottom).combined(with: .opacity), priority: 1)
29+
/// ```
30+
///
31+
/// - Parameters:
32+
/// - transition: The SwiftUI transition to apply.
33+
/// - priority: Animation priority (higher values animate first). Default is 0.
34+
/// - Returns: A view with staggered animation applied.
35+
public func stagger<T: Transition>(transition: T, priority: Double = 0) -> some View {
36+
modifier(StaggerViewModifier(transition: transition, priority: priority))
37+
}
38+
39+
/// Enables staggered animations for child views.
40+
///
41+
/// Apply this modifier to a container view to enable staggered animations
42+
/// for any child views that use the `stagger()` modifier.
43+
///
44+
/// Example:
45+
/// ```swift
46+
/// VStack {
47+
/// Text("Title").stagger(priority: 1)
48+
/// Text("Subtitle").stagger()
49+
/// Button("Action") { }.stagger(priority: -1)
50+
/// }
51+
/// .staggerContainer(
52+
/// configuration: StaggerConfiguration(
53+
/// baseDelay: 0.2,
54+
/// animationCurve: .spring()
55+
/// )
56+
/// )
57+
/// ```
58+
///
59+
/// - Parameter configuration: Configuration for customizing animation behavior. Default is `.init()`.
60+
/// - Returns: A view that coordinates staggered animations for its children.
61+
public func staggerContainer(
62+
configuration: StaggerConfiguration = .init()
63+
) -> some View {
64+
modifier(StaggerContainerViewModifier(configuration: configuration))
65+
}
66+
}

0 commit comments

Comments
 (0)