Cart Functionality for Cookies App with SwiftUI

Elena R.
5 min readMar 3, 2022

--

In the previous chapter, we learned how to display a list of products and how to implement navigation to the product details page. Let’s continue and see what it takes to add a Cart Functionality to our Cookies App.

To work with a cart we’ll need:

  • a cart object to store all the added items
  • a tab to navigate to the cart view
  • and a cart view to view, add or remove items.

Let’s first create a model class for our cart.

Observable Cart object in SwiftUI

Create a new Swift file inside the model folder and call it “Cart”. We’ll use an ObservableObject protocol to be able to use it in multiple views across our app. If one view changes cart items data, other views can reflect those changes accordingly.

import Foundationclass Cart: ObservableObject {   @Published var cartItems: [CartItem]   init() {      self.cartItems = []}}

Cart items will have a simple structure that has an id, a product, and its count:

import Foundationstruct CartItem: Identifiable {   var id: String   var product: Product   var count: Int
init(product: Product, count: Int) { self.id = UUID().uuidString self.product = product self.count = count}}

We’ll initialize a cart object in the app entry point (JerseyCookiesApp.Swift) and pass it to the content view:

@mainstruct JerseyCookiesApp: App {   var body: some Scene {   WindowGroup {      ContentView(cart: Cart())   }}}

In the ContentView (that is our top parent view) we’ll get our previously created cart object and set it as an environment object by calling the .environmentObject(cart) modifier:

struct ContentView: View {@ObservedObject var cart: Cartenum Tab {   case home   case catalog   case cart}@State private var selection: Tab = .homevar body: some View {   TabView(selection: $selection) {   HomeView().tabItem { Label("Home", systemImage: "star")}.tag(Tab.home)   CatalogView().tabItem { Label("Catalog", systemImage: "list.bullet")}.tag(Tab.catalog)   CartView().tabItem { Label("Cart", systemImage: "cart")}.tag(Tab.cart)}.environmentObject(cart)}}

Now in any view where we need access to the cart, we can get it as an Environmental Object:

@EnvironmentObject var cart: Cart

Adding Items to Cart

Let’s work on a simple test scenario — when a user first opens the app the cart is empty and after tapping the “Add to Cart” button the cart icon changes:

First, let’s add the addProduct method to the Cart Model class created earlier in this chapter:

func addProduct(product: Product){   var addNewProduct = true   for (index, item) in cartItems.enumerated() {      if item.product.id == product.id {         cartItems[index].count = cartItems[index].count + 1         addNewProduct = false      }   }   if addNewProduct {      cartItems.append(CartItem(product: product, count: 1))   }}

This method checks if we already have this product in our cart — then we increase the count for a corresponding cart item. Otherwise, we create a new cart item and add it to the cart.

This method will be called when a user taps the “Add to Cart” button on the product details page:

In the ProductDetail view update the button’s code:

Button(action: {cart.addProduct(product: product)}){   RoundedButton(imageName: "cart.badge.plus", text: "Add to Cart")}

In the ContentView update the tab item’s code for the cart icon to depend on whether our cart is empty or not:

CartView().tabItem {   Label("Cart", systemImage: cart.cartItems.count == 0 ? "cart" : "cart.badge.plus")}.tag(Tab.cart)

Now we can run a simulator and check our test scenario by adding a product to the cart and seeing the icon changes accordingly.

The next thing we’ll work on is a cart view that is displayed when a user selects the Cart tab.

Cart View

Similar to the Catalog view, the Cart view will contain a NavigationView with the list of cart item rows. When a user taps a cart item the app will show a cart item detail page.

The following code will get access to our cart object (that we set as an environment object earlier) and present the cart items as tappable rows:

struct CartView: View {   @EnvironmentObject var cart: Cart   var body: some View {   NavigationView {      List($cart.cartItems) { $cartItem in         NavigationLink(destination: CartItemDetail(cartItem: $cartItem)) {             CartItemRow(cartItem:  $cartItem)}      }.navigationTitle("Cart")   }.navigationViewStyle(StackNavigationViewStyle())}}

Now we need to add CartItemRow and CartItemDetail views to be able to run the simulator:

struct CartItemRow: View {   @Binding var cartItem: CartItem   var body: some View {   HStack {      cartItem.product.image.resizable().frame(width: 100, height: 100).clipShape(Circle())      VStack(alignment: .leading) {         Text(cartItem.product.name).fontWeight(.semibold)         Text("\(cartItem.product.price) | \(cartItem.product.calories)")         Button("Show details"){}.foregroundColor(.gray)      }      Spacer()      Text("\(cartItem.count)")   }}}

Both these views have a cartItem parameter for getting the data from the parent view to display the cart item content:

struct CartItemDetail: View {   @Binding var cartItem: CartItem   var body: some View {   VStack {      Text(cartItem.product.name).font(.largeTitle)      cartItem.product.image.resizable().frame(width: 200, height: 200).clipShape(Circle())      Text("\(cartItem.product.price) | \(cartItem.product.calories)")      Text(cartItem.product.description)
.multilineTextAlignment(.center).padding(.all, 20.0)
}}}

Let’s run a simulator and add a few products to the cart. Now we can see them in our cart and tap them to see the details:

Cart Summary

Now let’s work on the second part of the Cart View — Cart Summary. It will have an “Add Promo Code” button, display subtotal and total for our order, and we’ll also add a “Continue” button to proceed to the checkout screen.

To keep our CartView’s code simple and readable, let’s create a separate view for the cart summary:

struct CartSummary: View {   @Binding var subtotal: Double   var body: some View {   VStack {      Button(action: { print("We'll implement promo codes later")      }) { Text("Add promo code").padding()}      HStack {         Text("Summary").bold()         Spacer()         VStack {            HStack {               Text("Subtotal")               Spacer()               Text(String(format: "$%.2f", subtotal))            }            HStack {               Text("Taxes")               Spacer()               Text(String(format: "$%.2f", subtotal*0.0662))            }            HStack {               Text("Total")               Spacer()               Text(String(format: "$%.2f", subtotal+subtotal*0.0662))            }         }.frame(width: 200)      }.padding()   }.background(Color.gray.opacity(0.1))}}

Now, as you see this view depends on the subtotal value — therefore we need to add a subtotal property to our Cart model class and add a code for its calculation:

@Published var cartItems: [CartItem]var subtotal = 0.0

Inside the addProduct method add:

let price = Double(product.price) ?? 0.0subtotal = subtotal + price

And finally, let’s add the CartSummary view and Continue button to the CartView:

CartSummary(subtotal: $cart.subtotal)RoundedButton(imageName: "creditcard", text: "Continue").padding()

RoundedButton’s code is here:

struct RoundedButton: View {   var imageName: String   var text: String   var body: some View {   HStack {      Image(systemName: imageName).font(.title)      Text(text).fontWeight(.semibold).font(.title)   }.padding().foregroundColor(.white).background(Color.orange).cornerRadius(40)}}

And that’s it! Hope it was helpful and you enjoyed this tutorial — see you in the next chapters!

--

--