swiftui-animation by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-animation审查、编写和修复 SwiftUI 动画。使用 Swift 6.2 模式,应用具有正确时序、过渡和无障碍处理的现代动画 API。
| 类别 | API | 使用时机 |
|---|---|---|
| 状态驱动 | withAnimation, .animation(_:body:), .animation(_:value:) |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 显式状态变化、选择性修饰符动画或简单的值绑定变化 |
| 多阶段 | PhaseAnimator | 顺序多步骤动画 |
| 关键帧 | KeyframeAnimator | 复杂的多属性编排 |
| 共享元素 | matchedGeometryEffect | 布局驱动的英雄式过渡 |
| 导航 | matchedTransitionSource + .navigationTransition(.zoom) | NavigationStack 的 push/pop 缩放 |
| 视图生命周期 | .transition() | 插入和移除 |
| 文本内容 | .contentTransition() | 原地文本/数字变化 |
| 符号 | .symbolEffect() | SF 符号动画 |
| 自定义 | CustomAnimation 协议 | 新颖的时序曲线 |
// 时序曲线
.linear // 恒定速度
.easeIn(duration: 0.3) // 缓慢开始
.easeOut(duration: 0.3) // 缓慢结束
.easeInOut(duration: 0.3) // 缓慢开始和结束
// Spring 预设 (自然运动的首选)
.smooth // 无弹跳,流畅
.smooth(duration: 0.5, extraBounce: 0.0)
.snappy // 轻微弹跳,响应迅速
.snappy(duration: 0.4, extraBounce: 0.1)
.bouncy // 可见弹跳,活泼
.bouncy(duration: 0.5, extraBounce: 0.2)
// 自定义 spring
.spring(duration: 0.5, bounce: 0.3, blendDuration: 0.0)
.spring(Spring(duration: 0.6, bounce: 0.2), blendDuration: 0.0)
.interactiveSpring(response: 0.15, dampingFraction: 0.86)
withAnimation(.spring) { isExpanded.toggle() }
// 带完成回调 (iOS 17+)
withAnimation(.smooth(duration: 0.35), completionCriteria: .logicallyComplete) {
isExpanded = true
} completion: { loadContent() }
当只需要特定修饰符进行动画时,优先使用 .animation(_:body:)。对于可以一起动画化视图的可动画修饰符的简单值绑定变化,使用 .animation(_:value:)。
Badge()
.foregroundStyle(isActive ? .green : .secondary)
.animation(.snappy) { content in
content
.scaleEffect(isActive ? 1.15 : 1.0)
.opacity(isActive ? 1.0 : 0.7)
}
Circle()
.scaleEffect(isActive ? 1.2 : 1.0)
.opacity(isActive ? 1.0 : 0.6)
.animation(.bouncy, value: isActive)
四种初始化形式,对应不同的心智模型。
// 感知型 (首选)
Spring(duration: 0.5, bounce: 0.3)
// 物理型
Spring(mass: 1.0, stiffness: 100.0, damping: 10.0)
// 响应型
Spring(response: 0.5, dampingRatio: 0.7)
// 稳定型
Spring(settlingDuration: 1.0, dampingRatio: 0.8)
三种预设与 Animation 预设对应:.smooth, .snappy, .bouncy。
在离散阶段之间循环,每个阶段使用各自的动画曲线。
enum PulsePhase: CaseIterable {
case idle, grow, shrink
}
struct PulsingDot: View {
var body: some View {
PhaseAnimator(PulsePhase.allCases) { phase in
Circle()
.frame(width: 40, height: 40)
.scaleEffect(phase == .grow ? 1.4 : 1.0)
.opacity(phase == .shrink ? 0.5 : 1.0)
} animation: { phase in
switch phase {
case .idle: .easeIn(duration: 0.2)
case .grow: .spring(duration: 0.4, bounce: 0.3)
case .shrink: .easeOut(duration: 0.3)
}
}
}
}
基于触发器的变体在每次触发器变化时运行一个周期:
PhaseAnimator(PulsePhase.allCases, trigger: tapCount) { phase in
// ...
} animation: { _ in .spring(duration: 0.4) }
沿独立的时间线动画化多个属性。
struct AnimValues {
var scale: Double = 1.0
var yOffset: Double = 0.0
var opacity: Double = 1.0
}
struct BounceView: View {
@State private var trigger = false
var body: some View {
Image(systemName: "star.fill")
.font(.largeTitle)
.keyframeAnimator(
initialValue: AnimValues(),
trigger: trigger
) { content, value in
content
.scaleEffect(value.scale)
.offset(y: value.yOffset)
.opacity(value.opacity)
} keyframes: { _ in
KeyframeTrack(\.scale) {
SpringKeyframe(1.5, duration: 0.3)
CubicKeyframe(1.0, duration: 0.4)
}
KeyframeTrack(\.yOffset) {
CubicKeyframe(-30, duration: 0.2)
CubicKeyframe(0, duration: 0.4)
}
KeyframeTrack(\.opacity) {
LinearKeyframe(0.6, duration: 0.15)
LinearKeyframe(1.0, duration: 0.25)
}
}
.onTapGesture { trigger.toggle() }
}
}
关键帧类型:LinearKeyframe (线性), CubicKeyframe (平滑曲线), SpringKeyframe (弹簧物理), MoveKeyframe (瞬时跳跃)。
使用 repeating: true 进行循环关键帧动画。
替代手动的 AnimatableData 样板代码。附加到任何具有可动画存储属性的类型上。
// 错误:手动 AnimatableData (冗长,易错)
struct WaveShape: Shape, Animatable {
var frequency: Double
var amplitude: Double
var phase: Double
var animatableData: AnimatablePair<Double, AnimatablePair<Double, Double>> {
get { AnimatablePair(frequency, AnimatablePair(amplitude, phase)) }
set {
frequency = newValue.first
amplitude = newValue.second.first
phase = newValue.second.second
}
}
// ...
}
// 正确:@Animatable 宏合成 animatableData
@Animatable
struct WaveShape: Shape {
var frequency: Double
var amplitude: Double
var phase: Double
@AnimatableIgnored var lineWidth: CGFloat
func path(in rect: CGRect) -> Path {
// 使用 frequency, amplitude, phase 绘制波形
}
}
规则:
VectorArithmetic。@AnimatableIgnored 排除不可动画的属性。同步视图之间的几何信息,用于共享元素动画。
struct HeroView: View {
@Namespace private var heroSpace
@State private var isExpanded = false
var body: some View {
if isExpanded {
DetailCard()
.matchedGeometryEffect(id: "card", in: heroSpace)
.onTapGesture {
withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
isExpanded = false
}
}
} else {
ThumbnailCard()
.matchedGeometryEffect(id: "card", in: heroSpace)
.onTapGesture {
withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
isExpanded = true
}
}
}
}
}
为了使插值生效,每个 ID 在同一时间必须恰好有一个视图可见。
在源视图上使用 matchedTransitionSource,在目标视图上使用 .navigationTransition(.zoom(...))。
struct GalleryView: View {
@Namespace private var zoomSpace
let items: [GalleryItem]
var body: some View {
NavigationStack {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {
ForEach(items) { item in
NavigationLink {
GalleryDetail(item: item)
.navigationTransition(
.zoom(sourceID: item.id, in: zoomSpace)
)
} label: {
ItemThumbnail(item: item)
.matchedTransitionSource(
id: item.id, in: zoomSpace
)
}
}
}
}
}
}
}
在目标视图上应用 .navigationTransition,而不是在内部容器上。
控制视图在插入和移除时的动画方式。
if showBanner {
BannerView()
.transition(.move(edge: .top).combined(with: .opacity))
}
内置类型:.opacity, .slide, .scale, .scale(_:anchor:), .move(edge:), .push(from:), .offset(x:y:), .identity, .blurReplace, .blurReplace(_:), .symbolEffect, .symbolEffect(_:options:)。
非对称过渡:
.transition(.asymmetric(
insertion: .push(from: .bottom),
removal: .opacity
))
动画化原地内容变化,无需插入/移除。
Text("\(score)")
.contentTransition(.numericText(countsDown: false))
.animation(.snappy, value: score)
// 对于 SF 符号
Image(systemName: isMuted ? "speaker.slash" : "speaker.wave.3")
.contentTransition(.symbolEffect(.replace.downUp))
类型:.identity, .interpolate, .opacity, .numericText(countsDown:), .numericText(value:), .symbolEffect。
使用语义效果动画化 SF 符号。
// 离散型 (在值变化时触发)
Image(systemName: "bell.fill")
.symbolEffect(.bounce, value: notificationCount)
Image(systemName: "arrow.clockwise")
.symbolEffect(.wiggle.clockwise, value: refreshCount)
// 无限型 (在条件满足时持续活动)
Image(systemName: "wifi")
.symbolEffect(.pulse, isActive: isSearching)
Image(systemName: "mic.fill")
.symbolEffect(.breathe, isActive: isRecording)
// 可变颜色与链式效果
Image(systemName: "speaker.wave.3.fill")
.symbolEffect(
.variableColor.iterative.reversing.dimInactiveLayers,
options: .repeating,
isActive: isPlaying
)
所有效果:.bounce, .pulse, .variableColor, .scale, .appear, .disappear, .replace, .breathe, .rotate, .wiggle。
作用域:.byLayer, .wholeSymbol。方向因效果而异。
使用 .symbolRenderingMode(_:) 控制 SF 符号图层的着色方式。
| 模式 | 效果 | 使用时机 |
|---|---|---|
.monochrome | 单一颜色均匀应用 (默认) | 工具栏、与文本匹配的简单图标 |
.hierarchical | 单一颜色,通过不透明度图层增加深度 | 无需多种颜色的微妙深度 |
.multicolor | 系统为每个图层定义的固定颜色 | 天气、文件类型 — Apple 预设的调色板 |
.palette | 通过 .foregroundStyle 为每个图层自定义颜色 | 品牌颜色、自定义多色图标 |
// Hierarchical — 单一色调,不透明度图层增加深度
Image(systemName: "speaker.wave.3.fill")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.blue)
// Palette — 每个图层自定义颜色
Image(systemName: "person.crop.circle.badge.plus")
.symbolRenderingMode(.palette)
.foregroundStyle(.blue, .green)
// Multicolor — 系统定义的颜色
Image(systemName: "cloud.sun.rain.fill")
.symbolRenderingMode(.multicolor)
可变颜色: .symbolVariableColor(value:) 用于基于百分比的填充 (信号强度、音量):
Image(systemName: "wifi")
.symbolVariableColor(value: signalStrength) // 0.0–1.0
.animation(_:)// 作用域过宽 — 在视图变化时应用
.animation(.easeIn)
// 正确 — 将动画绑定到一个值
.animation(.easeIn, value: isVisible)
// 正确 — 将动画作用域限定到选定的修饰符
.animation(.easeIn) { content in
content.opacity(isVisible ? 1.0 : 0.0)
}
切勿在 keyframeAnimator / PhaseAnimator 的内容闭包内运行繁重计算 — 它们每帧都会执行。应在外部预计算,仅动画化视觉属性。
@Environment(\.accessibilityReduceMotion) private var reduceMotion
withAnimation(reduceMotion ? .none : .bouncy) { showDetail = true }
每个 ID 在同一时间应只有一个视图可见。两个具有相同 ID 的视图同时可见会导致未定义的布局。
// 错误
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation { isVisible = true } }
// 正确
withAnimation(.spring.delay(0.5)) { isVisible = true }
// 错误 — 没有动画,内容过渡无效
Text("\(count)").contentTransition(.numericText(countsDown: true))
// 正确 — 与动画配对
Text("\(count)")
.contentTransition(.numericText(countsDown: true))
.animation(.snappy, value: count)
在最外层的目标视图上应用 .navigationTransition(.zoom(sourceID:in:)),而不是在容器内部。
withAnimation 包装了状态变化;隐式动画使用 .animation(_:body:) 进行选择性修饰符作用域限定,或使用 .animation(_:value:) 并指定明确的绑定值matchedGeometryEffect 每个 ID 恰好有一个源;缩放使用匹配的 id/namespace@Animatable 宏替代手动 animatableDataaccessibilityReduceMotion;未使用 DispatchQueue/UIView.animate.transition();contentTransition 与动画配对,并使用最合适的、作用域最窄的隐式动画references/animation-advanced.md 了解 CustomAnimation 协议、完整的 Spring 变体、所有 Transition 类型、符号效果详情、Transaction 系统、UnitCurve 类型和性能指导。每周安装量
413
仓库
GitHub 星标数
269
首次出现
2026年3月3日
安全审计
安装于
codex408
gemini-cli405
cursor405
amp405
cline405
opencode405
Review, write, and fix SwiftUI animations. Apply modern animation APIs with correct timing, transitions, and accessibility handling using Swift 6.2 patterns.
| Category | API | When to use |
|---|---|---|
| State-driven | withAnimation, .animation(_:body:), .animation(_:value:) | Explicit state changes, selective modifier animation, or simple value-bound changes |
| Multi-phase | PhaseAnimator | Sequenced multi-step animations |
| Keyframe | KeyframeAnimator | Complex multi-property choreography |
| Shared element | matchedGeometryEffect | Layout-driven hero transitions |
| Navigation | matchedTransitionSource + .navigationTransition(.zoom) | NavigationStack push/pop zoom |
| View lifecycle | .transition() | Insertion and removal |
| Text content | .contentTransition() | In-place text/number changes |
| Symbol | .symbolEffect() | SF Symbol animations |
| Custom | CustomAnimation protocol | Novel timing curves |
// Timing curves
.linear // constant speed
.easeIn(duration: 0.3) // slow start
.easeOut(duration: 0.3) // slow end
.easeInOut(duration: 0.3) // slow start and end
// Spring presets (preferred for natural motion)
.smooth // no bounce, fluid
.smooth(duration: 0.5, extraBounce: 0.0)
.snappy // small bounce, responsive
.snappy(duration: 0.4, extraBounce: 0.1)
.bouncy // visible bounce, playful
.bouncy(duration: 0.5, extraBounce: 0.2)
// Custom spring
.spring(duration: 0.5, bounce: 0.3, blendDuration: 0.0)
.spring(Spring(duration: 0.6, bounce: 0.2), blendDuration: 0.0)
.interactiveSpring(response: 0.15, dampingFraction: 0.86)
withAnimation(.spring) { isExpanded.toggle() }
// With completion (iOS 17+)
withAnimation(.smooth(duration: 0.35), completionCriteria: .logicallyComplete) {
isExpanded = true
} completion: { loadContent() }
Prefer .animation(_:body:) when only specific modifiers should animate. Use .animation(_:value:) for simple value-bound changes that can animate the view's animatable modifiers together.
Badge()
.foregroundStyle(isActive ? .green : .secondary)
.animation(.snappy) { content in
content
.scaleEffect(isActive ? 1.15 : 1.0)
.opacity(isActive ? 1.0 : 0.7)
}
Circle()
.scaleEffect(isActive ? 1.2 : 1.0)
.opacity(isActive ? 1.0 : 0.6)
.animation(.bouncy, value: isActive)
Four initializer forms for different mental models.
// Perceptual (preferred)
Spring(duration: 0.5, bounce: 0.3)
// Physical
Spring(mass: 1.0, stiffness: 100.0, damping: 10.0)
// Response-based
Spring(response: 0.5, dampingRatio: 0.7)
// Settling-based
Spring(settlingDuration: 1.0, dampingRatio: 0.8)
Three presets mirror Animation presets: .smooth, .snappy, .bouncy.
Cycle through discrete phases with per-phase animation curves.
enum PulsePhase: CaseIterable {
case idle, grow, shrink
}
struct PulsingDot: View {
var body: some View {
PhaseAnimator(PulsePhase.allCases) { phase in
Circle()
.frame(width: 40, height: 40)
.scaleEffect(phase == .grow ? 1.4 : 1.0)
.opacity(phase == .shrink ? 0.5 : 1.0)
} animation: { phase in
switch phase {
case .idle: .easeIn(duration: 0.2)
case .grow: .spring(duration: 0.4, bounce: 0.3)
case .shrink: .easeOut(duration: 0.3)
}
}
}
}
Trigger-based variant runs one cycle per trigger change:
PhaseAnimator(PulsePhase.allCases, trigger: tapCount) { phase in
// ...
} animation: { _ in .spring(duration: 0.4) }
Animate multiple properties along independent timelines.
struct AnimValues {
var scale: Double = 1.0
var yOffset: Double = 0.0
var opacity: Double = 1.0
}
struct BounceView: View {
@State private var trigger = false
var body: some View {
Image(systemName: "star.fill")
.font(.largeTitle)
.keyframeAnimator(
initialValue: AnimValues(),
trigger: trigger
) { content, value in
content
.scaleEffect(value.scale)
.offset(y: value.yOffset)
.opacity(value.opacity)
} keyframes: { _ in
KeyframeTrack(\.scale) {
SpringKeyframe(1.5, duration: 0.3)
CubicKeyframe(1.0, duration: 0.4)
}
KeyframeTrack(\.yOffset) {
CubicKeyframe(-30, duration: 0.2)
CubicKeyframe(0, duration: 0.4)
}
KeyframeTrack(\.opacity) {
LinearKeyframe(0.6, duration: 0.15)
LinearKeyframe(1.0, duration: 0.25)
}
}
.onTapGesture { trigger.toggle() }
}
}
Keyframe types: LinearKeyframe (linear), CubicKeyframe (smooth curve), SpringKeyframe (spring physics), MoveKeyframe (instant jump).
Use repeating: true for looping keyframe animations.
Replaces manual AnimatableData boilerplate. Attach to any type with animatable stored properties.
// WRONG: Manual AnimatableData (verbose, error-prone)
struct WaveShape: Shape, Animatable {
var frequency: Double
var amplitude: Double
var phase: Double
var animatableData: AnimatablePair<Double, AnimatablePair<Double, Double>> {
get { AnimatablePair(frequency, AnimatablePair(amplitude, phase)) }
set {
frequency = newValue.first
amplitude = newValue.second.first
phase = newValue.second.second
}
}
// ...
}
// CORRECT: @Animatable macro synthesizes animatableData
@Animatable
struct WaveShape: Shape {
var frequency: Double
var amplitude: Double
var phase: Double
@AnimatableIgnored var lineWidth: CGFloat
func path(in rect: CGRect) -> Path {
// draw wave using frequency, amplitude, phase
}
}
Rules:
VectorArithmetic.@AnimatableIgnored to exclude non-animatable properties.Synchronize geometry between views for shared-element animations.
struct HeroView: View {
@Namespace private var heroSpace
@State private var isExpanded = false
var body: some View {
if isExpanded {
DetailCard()
.matchedGeometryEffect(id: "card", in: heroSpace)
.onTapGesture {
withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
isExpanded = false
}
}
} else {
ThumbnailCard()
.matchedGeometryEffect(id: "card", in: heroSpace)
.onTapGesture {
withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
isExpanded = true
}
}
}
}
}
Exactly one view per ID must be visible at a time for the interpolation to work.
Pair matchedTransitionSource on the source view with .navigationTransition(.zoom(...)) on the destination.
struct GalleryView: View {
@Namespace private var zoomSpace
let items: [GalleryItem]
var body: some View {
NavigationStack {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {
ForEach(items) { item in
NavigationLink {
GalleryDetail(item: item)
.navigationTransition(
.zoom(sourceID: item.id, in: zoomSpace)
)
} label: {
ItemThumbnail(item: item)
.matchedTransitionSource(
id: item.id, in: zoomSpace
)
}
}
}
}
}
}
}
Apply .navigationTransition on the destination view, not on inner containers.
Control how views animate on insertion and removal.
if showBanner {
BannerView()
.transition(.move(edge: .top).combined(with: .opacity))
}
Built-in types: .opacity, .slide, .scale, .scale(_:anchor:), .move(edge:), .push(from:), .offset(x:y:), .identity, .blurReplace, .blurReplace(_:), .symbolEffect, .symbolEffect(_:options:).
Asymmetric transitions:
.transition(.asymmetric(
insertion: .push(from: .bottom),
removal: .opacity
))
Animate in-place content changes without insertion/removal.
Text("\(score)")
.contentTransition(.numericText(countsDown: false))
.animation(.snappy, value: score)
// For SF Symbols
Image(systemName: isMuted ? "speaker.slash" : "speaker.wave.3")
.contentTransition(.symbolEffect(.replace.downUp))
Types: .identity, .interpolate, .opacity, .numericText(countsDown:), .numericText(value:), .symbolEffect.
Animate SF Symbols with semantic effects.
// Discrete (triggers on value change)
Image(systemName: "bell.fill")
.symbolEffect(.bounce, value: notificationCount)
Image(systemName: "arrow.clockwise")
.symbolEffect(.wiggle.clockwise, value: refreshCount)
// Indefinite (active while condition holds)
Image(systemName: "wifi")
.symbolEffect(.pulse, isActive: isSearching)
Image(systemName: "mic.fill")
.symbolEffect(.breathe, isActive: isRecording)
// Variable color with chaining
Image(systemName: "speaker.wave.3.fill")
.symbolEffect(
.variableColor.iterative.reversing.dimInactiveLayers,
options: .repeating,
isActive: isPlaying
)
All effects: .bounce, .pulse, .variableColor, .scale, .appear, .disappear, .replace, .breathe, .rotate, .wiggle.
Scope: .byLayer, .wholeSymbol. Direction varies per effect.
Control how SF Symbol layers are colored with .symbolRenderingMode(_:).
| Mode | Effect | When to use |
|---|---|---|
.monochrome | Single color applied uniformly (default) | Toolbars, simple icons matching text |
.hierarchical | Single color with opacity layers for depth | Subtle depth without multiple colors |
.multicolor | System-defined fixed colors per layer | Weather, file types — Apple's intended palette |
.palette | Custom colors per layer via .foregroundStyle | Brand colors, custom multi-color icons |
// Hierarchical — single tint, opacity layers for depth
Image(systemName: "speaker.wave.3.fill")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.blue)
// Palette — custom color per layer
Image(systemName: "person.crop.circle.badge.plus")
.symbolRenderingMode(.palette)
.foregroundStyle(.blue, .green)
// Multicolor — system-defined colors
Image(systemName: "cloud.sun.rain.fill")
.symbolRenderingMode(.multicolor)
Variable color: .symbolVariableColor(value:) for percentage-based fill (signal strength, volume):
Image(systemName: "wifi")
.symbolVariableColor(value: signalStrength) // 0.0–1.0
.animation(_:) when you need precise scope// TOO BROAD — applies when the view changes
.animation(.easeIn)
// CORRECT — bind animation to one value
.animation(.easeIn, value: isVisible)
// CORRECT — scope animation to selected modifiers
.animation(.easeIn) { content in
content.opacity(isVisible ? 1.0 : 0.0)
}
Never run heavy computation in keyframeAnimator / PhaseAnimator content closures — they execute every frame. Precompute outside, animate only visual properties.
@Environment(\.accessibilityReduceMotion) private var reduceMotion
withAnimation(reduceMotion ? .none : .bouncy) { showDetail = true }
Only one view per ID should be visible at a time. Two visible views with the same ID causes undefined layout.
// WRONG
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation { isVisible = true } }
// CORRECT
withAnimation(.spring.delay(0.5)) { isVisible = true }
// WRONG — no animation, content transition has no effect
Text("\(count)").contentTransition(.numericText(countsDown: true))
// CORRECT — pair with animation
Text("\(count)")
.contentTransition(.numericText(countsDown: true))
.animation(.snappy, value: count)
Apply .navigationTransition(.zoom(sourceID:in:)) on the outermost destination view, not inside a container.
withAnimation wraps the state change; implicit animation uses .animation(_:body:) for selective modifier scope or .animation(_:value:) with an explicit valuematchedGeometryEffect has exactly one source per ID; zoom uses matching id/namespace@Animatable macro used instead of manual animatableDataaccessibilityReduceMotion checked; no DispatchQueue/references/animation-advanced.md for CustomAnimation protocol, full Spring variants, all Transition types, symbol effect details, Transaction system, UnitCurve types, and performance guidance.Weekly Installs
413
Repository
GitHub Stars
269
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex408
gemini-cli405
cursor405
amp405
cline405
opencode405
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
UIView.animate.transition(); contentTransition is paired with animation and uses the narrowest implicit animation scope that fits