This posting is based on Kavsoft video:
https://youtu.be/GcEdCDPLaQM?si=aGU1aCVdJHQMPUes
(1) StaggeredView.swift
import SwiftUI
struct StaggeredView<Content: View>: View {
var config: StaggeredConfig = .init()
@ViewBuilder var content: Content
var body: some View {
Group(subviews: content) { subviewsCollection in
ForEach(subviewsCollection.indices, id: \.self) { index in
subviewsCollection[index]
.transition(CustomStaggredTransition(index: index, config: config))
}
}
}
}
fileprivate struct CustomStaggredTransition: Transition {
var index: Int
var config: StaggeredConfig
func body(content: Content, phase: TransitionPhase) -> some View {
let animationDelay: Double = min(Double(index) * config.delay, config.maxDelay)
let isIdentity: Bool = phase == .identity
let didDisappear: Bool = phase == .didDisappear
let x: CGFloat = config.offset.width
let y: CGFloat = config.offset.height
let reverseX: CGFloat = config.disappearInSameDirection ? x : -x
let disableX: CGFloat = config.noDisappearAnimation ? 0 : reverseX
let reverseY: CGFloat = config.disappearInSameDirection ? y : -y
let disableY: CGFloat = config.noDisappearAnimation ? 0 : reverseY
let offsetX = isIdentity ? 0 : didDisappear ? disableX : x
let offsetY = isIdentity ? 0 : didDisappear ? disableY : y
content
.opacity(isIdentity ? 1 : 0)
.blur(radius: isIdentity ? 0 : config.blurRadius)
.compositingGroup()
.scaleEffect(
isIdentity ? 1 : config.scale,
anchor: config.scaleAnchor
)
.offset(x: offsetX, y: offsetY)
.animation(config.animation.delay(animationDelay), value: phase)
}
}
struct StaggeredConfig {
var delay: Double = 0.05
var maxDelay: Double = 0.4
var blurRadius: CGFloat = 6
var offset: CGSize = .init(width: 0, height: 100)
var scale: CGFloat = 0.95
var scaleAnchor: UnitPoint = .center
var animation: Animation = .smooth(duration: 0.3, extraBounce: 0)
var disappearInSameDirection: Bool = false
var noDisappearAnimation: Bool = false
}
#Preview {
ContentView()
}
(2) ContentView.swift
import SwiftUI
struct ContentView: View {
@State private var showView: Bool = false
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 12) {
Button("Toggle View") {
showView.toggle()
}
// You can adjust StaggeredConfig
let config = StaggeredConfig(
offset: .zero,
scale: 0.85,
scaleAnchor: .center
)
// let config = StaggeredConfig(
// offset: .init(width: 0, height: 70),
// scale: 0.85,
// scaleAnchor: .center
// )
// MARK: Sample 1 - Grid View
LazyVGrid(columns: Array(repeating: GridItem(), count: 2)) {
StaggeredView(config: config) {
if showView {
ForEach(1...10, id: \.self) { _ in
RoundedRectangle(cornerRadius: 15)
.fill(.black.gradient)
.frame(height: 150)
}
}
}
}
// MARK: Sample 2 - DummyView
// StaggeredView(config: config) {
// if showView {
// ForEach(1...10, id: \.self) { _ in
// DummyView()
// }
// }
// }
Spacer(minLength: 0)
}
.padding(15)
.frame(maxWidth: .infinity) /// Makes sure that the width of the view stays same even whene there is no views are present!
}
.navigationTitle("Staggered View")
.navigationBarTitleDisplayMode(.inline)
}
}
/// Dummy View
@ViewBuilder
func DummyView() -> some View {
HStack(spacing: 10) {
Circle()
.frame(width: 45, height: 45)
VStack(alignment: .leading, spacing: 6) {
RoundedRectangle(cornerRadius: 5)
.frame(height: 10)
.padding(.trailing, 20)
RoundedRectangle(cornerRadius: 5)
.frame(height: 10)
.padding(.trailing, 140)
RoundedRectangle(cornerRadius: 5)
.frame(width: 100, height: 10)
}
}
.foregroundStyle(.gray.opacity(0.7).gradient)
}
}
#Preview {
ContentView()
}
you can download source code here:
'iOS > SwiftUI' 카테고리의 다른 글
[iOS / SwiftUI] VisionOS Style Menu Bar (0) | 2025.03.22 |
---|---|
[iOS / SwiftUI] DownsizedImage (0) | 2025.03.03 |
[iOS / SwiftUI] SwiftUI Notification Deep Linking (0) | 2025.02.16 |
[iOS / SwiftUI] SwiftUI Custom Alerts (0) | 2025.02.08 |
[iOS / SwiftUI] Use Deep Link in SwiftUI (0) | 2022.01.11 |