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, .default replaces the repeating animation and the card returns to rest.
  • Remove animation: Wrapping items.removeAll in withAnimation triggers the default removal transition for the grid item.
  • Drag to reorder: For iOS 16+, add .draggable(item.id) and .dropDestination modifiers 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)

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.