What You Will Build
An adaptive grid of colored cards that supports an edit mode. In edit mode, cards wiggle with a repeating animation and show a red minus button to remove them. A dashed "plus" card lets you add new items at any time. A toolbar button toggles between Edit and Done states.
Why This Pattern Matters
Editable grids are the foundation of home screen customization, photo album management, and widget configuration screens. This pattern teaches LazyVGrid with adaptive columns, repeating wiggle animations for edit mode, and smooth insert/remove transitions with withAnimation.
Step 1: The Grid Item Model
private struct GridItem_: Identifiable {
let id = UUID()
let name: String
let color: Color
}
Step 2: The Grid Layout with Edit Mode
struct EditableGridDemo: View {
@State private var items: [GridItem_] = (1...8).map {
GridItem_(name: "Item \($0)",
color: [Color.red, .blue, .green, .orange,
.purple, .cyan, .pink, .indigo][$0 - 1])
}
@State private var isEditing = false
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100),
spacing: 12)], spacing: 12) {
ForEach(items) { item in
ZStack(alignment: .topTrailing) {
RoundedRectangle(cornerRadius: 12)
.fill(item.color.gradient)
.frame(height: 100)
.overlay {
Text(item.name)
.font(.caption.bold())
.foregroundStyle(.white)
}
.scaleEffect(isEditing ? 0.95 : 1)
.animation(
isEditing
? .easeInOut(duration: 0.15)
.repeatForever(autoreverses: true)
: .default,
value: isEditing
)
if isEditing {
Button {
withAnimation {
items.removeAll { $0.id == item.id }
}
} label: {
Image(systemName: "minus.circle.fill")
.foregroundStyle(.white, .red)
.font(.title3)
}
.offset(x: 6, y: -6)
.transition(.scale)
}
}
}
// Add button
Button {
let newItem = GridItem_(
name: "New \(items.count + 1)",
color: [Color.red, .blue, .green,
.orange, .purple].randomElement()!)
withAnimation { items.append(newItem) }
} label: {
RoundedRectangle(cornerRadius: 12)
.strokeBorder(style: StrokeStyle(
lineWidth: 2, dash: [8]))
.foregroundStyle(.secondary)
.frame(height: 100)
.overlay {
Image(systemName: "plus")
.font(.title)
.foregroundStyle(.secondary)
}
}
}
.padding()
}
.toolbar {
Button(isEditing ? "Done" : "Edit") {
withAnimation { isEditing.toggle() }
}
}
}
}
How the Wiggle Animation Works
The wiggle is created by a repeating scaleEffect animation. When isEditing becomes true, each card continuously scales between 0.95 and 1.0 with autoreverses: true. When editing ends, the .default animation smoothly returns cards to scale 1.0 and stops the repeat.
Tips and Pitfalls
- GridItem(.adaptive(minimum: 100)) automatically calculates the number of columns based on available width. On iPhone you get 3 columns; on iPad you get more. No manual column count needed.
- The wiggle animation conditional: Using a ternary for the animation value is the key pattern. When isEditing is false,
.defaultreplaces the repeating animation and the card returns to rest. - Remove animation: Wrapping
items.removeAllin withAnimation triggers the default removal transition for the grid item. - Drag to reorder: For iOS 16+, add
.draggable(item.id)and.dropDestinationmodifiers to enable rearranging items by drag and drop. - Performance: LazyVGrid only creates views for visible items. This approach works smoothly with hundreds of items.
iOS Version: iOS 16+ (uses .gradient modifier)