SwiftUI Patterns

Building a Shopping Cart with EnvironmentObject

June 30, 2026
6 min read
Featured image for blog post: Building a Shopping Cart with EnvironmentObject

EnvironmentObject lets you share state across an entire view hierarchy. Here's a complete shopping cart implementation.

Follow along with the code: iOS-Practice on GitHub

The Cart Manager

class CartManager: ObservableObject {
    @Published var items: [CartItem] = []

    var totalItems: Int {
        items.reduce(0) { $0 + $1.quantity }
    }

    var totalPrice: Double {
        items.reduce(0) { $0 + ($1.product.price * Double($1.quantity)) }
    }

    func addToCart(_ product: Product) {
        if let index = items.firstIndex(where: { $0.product.id == product.id }) {
            items[index].quantity += 1
        } else {
            items.append(CartItem(product: product, quantity: 1))
        }
    }

    func removeFromCart(_ product: Product) {
        if let index = items.firstIndex(where: { $0.product.id == product.id }) {
            if items[index].quantity > 1 {
                items[index].quantity -= 1
            } else {
                items.remove(at: index)
            }
        }
    }

    func clearCart() {
        items.removeAll()
    }
}

struct CartItem: Identifiable {
    let id = UUID()
    let product: Product
    var quantity: Int
}

Root View: Create and Inject

struct ShopView: View {
    @StateObject private var cartManager = CartManager()
    @State private var products: [Product] = []
    @State private var showCart = false

    var body: some View {
        List {
            ForEach(products) { product in
                ProductCartRow(product: product)
            }
        }
        .navigationTitle("Shop")
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                CartBadgeButton(showCart: $showCart)
            }
        }
        .sheet(isPresented: $showCart) {
            CartView()
        }
        .environmentObject(cartManager)  // Inject here
        .task {
            await loadProducts()
        }
    }
}

Cart Badge Button

struct CartBadgeButton: View {
    @Binding var showCart: Bool
    @EnvironmentObject var cartManager: CartManager

    var body: some View {
        Button {
            showCart = true
        } label: {
            ZStack(alignment: .topTrailing) {
                Image(systemName: "cart")

                if cartManager.totalItems > 0 {
                    Text("\(cartManager.totalItems)")
                        .font(.caption2)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                        .padding(4)
                        .background(Color.red)
                        .clipShape(Circle())
                        .offset(x: 8, y: -8)
                }
            }
        }
    }
}

Product Row with Add/Remove

struct ProductCartRow: View {
    let product: Product
    @EnvironmentObject var cartManager: CartManager

    var quantityInCart: Int {
        cartManager.items.first(where: { $0.product.id == product.id })?.quantity ?? 0
    }

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(product.name).font(.headline)
                Text("$\(product.price, specifier: "%.0f")").foregroundColor(.green)
            }

            Spacer()

            if quantityInCart > 0 {
                HStack(spacing: 12) {
                    Button { cartManager.removeFromCart(product) } label: {
                        Image(systemName: "minus.circle.fill").foregroundColor(.red)
                    }
                    Text("\(quantityInCart)").fontWeight(.semibold)
                    Button { cartManager.addToCart(product) } label: {
                        Image(systemName: "plus.circle.fill").foregroundColor(.green)
                    }
                }
                .buttonStyle(.plain)
            } else {
                Button("Add") { cartManager.addToCart(product) }
                    .buttonStyle(.bordered)
            }
        }
    }
}

Cart View

struct CartView: View {
    @EnvironmentObject var cartManager: CartManager
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        NavigationStack {
            Group {
                if cartManager.items.isEmpty {
                    ContentUnavailableView("Cart Empty", systemImage: "cart")
                } else {
                    List {
                        ForEach(cartManager.items) { item in
                            CartItemRow(item: item)
                        }
                        .onDelete { indexSet in
                            cartManager.items.remove(atOffsets: indexSet)
                        }

                        Section {
                            HStack {
                                Text("Total").font(.headline)
                                Spacer()
                                Text("$\(cartManager.totalPrice, specifier: "%.2f")")
                                    .font(.title2)
                                    .fontWeight(.bold)
                            }
                        }

                        Button("Checkout") { }
                            .buttonStyle(.borderedProminent)

                        Button("Clear Cart", role: .destructive) {
                            cartManager.clearCart()
                        }
                    }
                }
            }
            .navigationTitle("Cart (\(cartManager.totalItems))")
            .toolbar {
                Button("Done") { dismiss() }
            }
        }
    }
}

Key Points

  1. @StateObject at the root creates the manager
  2. .environmentObject() injects it
  3. @EnvironmentObject accesses it anywhere below
  4. Changes propagate automatically to all views

Interview Tip

This demonstrates complete understanding of SwiftUI state management: ownership, injection, and reactive updates.

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch