Building an E-commerce App with SwiftUI

SwiftUI is Apple’s declarative UI framework that makes building user interfaces on iOS, macOS, watchOS, and tvOS more intuitive and efficient. This comprehensive guide walks you through building an e-commerce app using SwiftUI. We’ll cover setting up the project, designing the UI, managing data, implementing cart functionality, and integrating with external services.

Introduction to SwiftUI and E-commerce Apps

SwiftUI offers a modern approach to UI development, enabling you to create dynamic and responsive interfaces with less code compared to UIKit. Building an e-commerce app involves designing product listings, handling user authentication, managing a shopping cart, and integrating payment gateways. By leveraging SwiftUI, you can achieve a seamless and engaging user experience.

Setting Up the Project

Step 1: Create a New Xcode Project

Open Xcode and select “Create a new Xcode project”. Choose “iOS” and then select “App”.

\"Create

Enter your project details (Product Name, Organization Identifier, and Interface: SwiftUI). Choose a suitable location and create the project.

Step 2: Project Structure

Your initial project structure should include:

  • [Project Name]App.swift: The entry point of your application.
  • ContentView.swift: A default view where you can start building your UI.
  • Assets.xcassets: Where you can store images and colors.

\"Xcode

Designing the UI with SwiftUI

1. Product Listing View

Create a Product struct to hold product details.


import SwiftUI

struct Product: Identifiable {
    let id: UUID = UUID()
    let name: String
    let description: String
    let price: Double
    let imageName: String
}

Next, let’s build a sample product list:


class ProductList: ObservableObject {
    @Published var products: [Product] = [
        Product(name: "Vintage T-Shirt", description: "Classic fit vintage t-shirt made from 100% organic cotton.", price: 29.99, imageName: "tshirt1"),
        Product(name: "Leather Jacket", description: "Genuine leather jacket perfect for any occasion.", price: 199.99, imageName: "jacket1"),
        Product(name: "Wool Sweater", description: "Cozy wool sweater to keep you warm in the winter.", price: 79.99, imageName: "sweater1")
    ]
}

Now, let’s design the ProductListView using SwiftUI’s List and NavigationLink:


import SwiftUI

struct ProductListView: View {
    @ObservedObject var productList = ProductList()

    var body: some View {
        NavigationView {
            List(productList.products) { product in
                NavigationLink(destination: ProductDetailView(product: product)) {
                    HStack {
                        Image(product.imageName)
                            .resizable()
                            .frame(width: 50, height: 50)
                        VStack(alignment: .leading) {
                            Text(product.name)
                                .font(.headline)
                            Text("$\(product.price, specifier: "%.2f")")
                                .font(.subheadline)
                        }
                    }
                }
            }
            .navigationTitle("Products")
        }
    }
}

You will also need to create ProductDetailView which will display further product information upon selection


import SwiftUI

struct ProductDetailView: View {
    let product: Product

    var body: some View {
        VStack {
            Image(product.imageName)
                .resizable()
                .scaledToFit()
                .padding()

            Text(product.name)
                .font(.title)
                .padding(.bottom, 5)

            Text(product.description)
                .font(.body)
                .padding(.horizontal)

            Text("Price: $\(product.price, specifier: "%.2f")")
                .font(.headline)
                .padding(.top, 10)

            Spacer()
        }
        .navigationTitle(product.name)
    }
}

Update ContentView to show the ProductListView:


struct ContentView: View {
    var body: some View {
        ProductListView()
    }
}

Remember to add product images (e.g., tshirt1.jpg, jacket1.jpg, sweater1.jpg) to your Assets.xcassets folder. Drag images from your computer to your Assets to add them.

2. Shopping Cart View

Implement the shopping cart functionality using a CartManager class:


import SwiftUI

class CartManager: ObservableObject {
    @Published var cartItems: [Product] = []
    @Published var total: Double = 0.0

    func addToCart(product: Product) {
        cartItems.append(product)
        total += product.price
    }

    func removeFromCart(product: Product) {
        if let index = cartItems.firstIndex(where: { $0.id == product.id }) {
            total -= cartItems[index].price
            cartItems.remove(at: index)
        }
    }
}

Create a CartView to display the cart items and total amount:


import SwiftUI

struct CartView: View {
    @ObservedObject var cartManager: CartManager

    var body: some View {
        NavigationView {
            List {
                ForEach(cartManager.cartItems) { item in
                    HStack {
                        Text(item.name)
                        Spacer()
                        Text("$\(item.price, specifier: "%.2f")")
                    }
                }
                .onDelete(perform: deleteItem)

                HStack {
                    Text("Total:")
                        .font(.headline)
                    Spacer()
                    Text("$\(cartManager.total, specifier: "%.2f")")
                        .font(.headline)
                }
            }
            .navigationTitle("Cart")
        }
    }

    func deleteItem(at offsets: IndexSet) {
        offsets.forEach { index in
            let productToRemove = cartManager.cartItems[index]
            cartManager.removeFromCart(product: productToRemove)
        }
    }
}

Add an “Add to Cart” button in ProductDetailView. Also pass the cartManager environment object down:


import SwiftUI

struct ProductDetailView: View {
    let product: Product
    @EnvironmentObject var cartManager: CartManager  // Retrieve the cart manager

    var body: some View {
        VStack {
            Image(product.imageName)
                .resizable()
                .scaledToFit()
                .padding()

            Text(product.name)
                .font(.title)
                .padding(.bottom, 5)

            Text(product.description)
                .font(.body)
                .padding(.horizontal)

            Text("Price: $\(product.price, specifier: "%.2f")")
                .font(.headline)
                .padding(.top, 10)
            Button {
                cartManager.addToCart(product: product) // Access through environment
            } label: {
                Text("Add to Cart")
            }

            Spacer()
        }
        .navigationTitle(product.name)
    }
}

Final edit to ContentView.swift:


import SwiftUI

struct ContentView: View {
    @StateObject var cartManager = CartManager()

    var body: some View {
        TabView {
            ProductListView()
                .tabItem {
                    Label("Products", systemImage: "list.dash")
                }
                .environmentObject(cartManager) // Inject into ProductListView and downstream views

            CartView(cartManager: cartManager)
                .tabItem {
                    Label("Cart", systemImage: "cart.fill")
                }
        }
    }
}

3. Implementing User Authentication

You can integrate Firebase Authentication for user registration, login, and management. Install the Firebase SDK via Swift Package Manager:

  1. Go to File > Add Packages.
  2. Enter https://github.com/firebase/firebase-ios-sdk.
  3. Select the required Firebase libraries (FirebaseAuth and FirebaseFirestore are essential).

Create AuthManager class and implement:


import SwiftUI
import Firebase

class AuthManager: ObservableObject {
    @Published var isLoggedIn: Bool = false

    init() {
        FirebaseApp.configure() //Configure Firebase
        Auth.auth().addStateDidChangeListener { auth, user in
            if user != nil {
                self.isLoggedIn = true
            } else {
                self.isLoggedIn = false
            }
        }
    }

    func signUp(email: String, password: String) {
        Auth.auth().createUser(withEmail: email, password: password) { result, error in
            if let error = error {
                print("Sign up failed: \(error.localizedDescription)")
            } else {
                self.isLoggedIn = true
                print("Sign up successful!")
            }
        }
    }

    func signIn(email: String, password: String) {
        Auth.auth().signIn(withEmail: email, password: password) { result, error in
            if let error = error {
                print("Sign in failed: \(error.localizedDescription)")
            } else {
                self.isLoggedIn = true
                print("Sign in successful!")
            }
        }
    }

    func signOut() {
        do {
            try Auth.auth().signOut()
            self.isLoggedIn = false
            print("Signed out!")
        } catch {
            print("Sign out failed: \(error.localizedDescription)")
        }
    }
}

4. Setting up Auth Screens (SignIn & SignUp)

Create Views for signing in, using the AuthManager as an injected environment variable.


struct SignUpView: View {
    @State private var email = ""
    @State private var password = ""
    @EnvironmentObject var authManager: AuthManager
    
    var body: some View {
        VStack {
            TextField("Email", text: $email)
                .padding()
                .keyboardType(.emailAddress)
                .autocapitalization(.none)
            
            SecureField("Password", text: $password)
                .padding()
            
            Button("Sign Up") {
                authManager.signUp(email: email, password: password)
            }
            .padding()
        }
        .navigationTitle("Sign Up")
    }
}


struct SignInView: View {
    @State private var email = ""
    @State private var password = ""
    @EnvironmentObject var authManager: AuthManager
    
    var body: some View {
        VStack {
            TextField("Email", text: $email)
                .padding()
                .keyboardType(.emailAddress)
                .autocapitalization(.none)
            
            SecureField("Password", text: $password)
                .padding()
            
            Button("Sign In") {
                authManager.signIn(email: email, password: password)
            }
            .padding()
        }
        .navigationTitle("Sign In")
    }
}

5. Update ContentView with login check

Add a boolean value which listens for a login to determine ContentView displayed.


import SwiftUI

struct ContentView: View {
    @StateObject var cartManager = CartManager()
    @StateObject var authManager = AuthManager() // Keep the state managed
    
    var body: some View {
        // Based on isLoggedIn flag display alternative View
        if authManager.isLoggedIn {
            TabView {
                ProductListView()
                    .tabItem {
                        Label("Products", systemImage: "list.dash")
                    }
                    .environmentObject(cartManager)
                
                CartView(cartManager: cartManager)
                    .tabItem {
                        Label("Cart", systemImage: "cart.fill")
                    }
            }
            .environmentObject(authManager) // Provide Authentication data downstream to TabView
        } else {
            NavigationView {
                VStack {
                    Text("Welcome! Please Sign In or Sign Up.")
                        .padding()
                    
                    NavigationLink("Sign In", destination: SignInView())
                        .padding()
                        .environmentObject(authManager)
                    
                    NavigationLink("Sign Up", destination: SignUpView())
                        .padding()
                        .environmentObject(authManager)
                }
                .navigationTitle("Authentication")
            }
        }
    }
}

4. Integrating Payment Gateway

Integrating a payment gateway like Stripe or PayPal allows users to make purchases. Since dealing with real transactions requires more attention to security and proper integration, let’s focus on where and how to inject it into a dummy function (as real information would need secrets keys etc.)

Create an initial struct which manages our order, e.g.:


struct Order {
    var items: [Product]
    var totalAmount: Double
}

Within CartManager.swift:


func checkout() -> Order? {
        // Simulate basic validation (e.g., cart not empty, user logged in, etc.)
        if cartItems.isEmpty {
            print("Cart is empty. Can't proceed with checkout.")
            return nil
        }
        
        // Here, integrate with your chosen payment gateway
        // E.g., call a function that integrates with Stripe/PayPal to create paymentIntent and capture payment
        
        let simulatedOrderId = simulatePaymentProcessing()  // Use a valid transaction/payment ID
        
        // After successful payment: Create an Order object for processing
        let newOrder = Order(items: cartItems, totalAmount: total)
        
        // Process the new order details using a separate handler.
        handleNewOrder(order: newOrder)
        
        // Empty the cart to complete the order process.
        emptyCart()
        
        // Now with more order details as the function return for external services
        return newOrder
    }
    
    private func simulatePaymentProcessing() -> String {
        // Placeholder for real API integration call
        return "dummy-order-id-" + String(Int.random(in: 1000...9999))
    }

    private func handleNewOrder(order: Order) {
        // Send to your own service or storage such as cloud
        print("Details: \(order.items.count) \(order.totalAmount)")
        
        // Clear and ready
        self.emptyCart()
    }
    
    private func emptyCart() {
        self.cartItems = []
        self.total = 0
    }

You can simulate and add that final function within CartView e.g.


Button {
    if let order = cartManager.checkout() {
        print("Processing order \(order)...")
    }
} label: {
    Text("Checkout")
        .font(.headline)
        .foregroundColor(.white)
        .padding()
        .background(Color.blue)
        .cornerRadius(10)
}

Additional Features and Best Practices

  • Animations: Use withAnimation to add smooth transitions between views.
  • Accessibility: Implement accessibility features using AccessibilityLabel, AccessibilityHint, etc.
  • State Management: Consider using advanced state management solutions like Redux or Combine for more complex apps.

Conclusion

Building an e-commerce app with SwiftUI offers an efficient and modern way to create engaging user interfaces on Apple platforms. By utilizing SwiftUI’s declarative syntax, managing data effectively, and integrating essential functionalities, you can develop a seamless e-commerce experience. This guide covers essential steps and practices to help you get started, paving the way for building a robust and feature-rich app.