SwiftUI Patterns
Building a Shopping Cart with EnvironmentObject
June 30, 2026
6 min read

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
- @StateObject at the root creates the manager
- .environmentObject() injects it
- @EnvironmentObject accesses it anywhere below
- Changes propagate automatically to all views
Interview Tip
This demonstrates complete understanding of SwiftUI state management: ownership, injection, and reactive updates.