SwiftUI ShopViewModel: Products, Cart & Favorites!

by Alex Johnson 51 views

In this article, we'll explore the implementation of a ShopViewModel class in Swift, designed to manage products, cart items, and user favorites within a SwiftUI application. This class leverages the @Observable macro for seamless integration with SwiftUI's data binding, ensuring that UI updates are automatically triggered whenever the underlying data changes. Let's dive into the details of each component and how they work together to create a robust shopping experience.

Understanding the Core Components

Before we delve into the code, let's break down the key components of the ShopViewModel:

  • Products: Manages a list of products fetched from an API.
  • Cart: Handles adding, updating, and removing items from the shopping cart.
  • Favorites: Allows users to mark products as favorites and persist these preferences locally.
  • Toast Messages: Provides visual feedback to the user through transient messages.

Defining the CartItem Struct

First, we define a CartItem struct to represent an item in the shopping cart. This struct conforms to Identifiable and Equatable, making it easy to manage and compare items within the cart.

import Foundation
import SwiftUI

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

The CartItem struct includes:

  • id: A unique identifier for each cart item, generated using UUID.
  • product: The Product instance associated with the cart item.
  • quantity: The number of units of the product in the cart.

The ShopViewModel Class

The ShopViewModel class is the heart of our shopping application. It is marked with the @Observable macro, which automatically makes its properties observable, allowing SwiftUI views to react to changes in these properties.

@MainActor
@Observable
class ShopViewModel {

    // MARK: - Propiedades de la Tienda
    var products: [Product] = []
    var isLoading: Bool = false
    var errorMessage: String?
    
    // URL de la API
    private let apiURL = "https://petapi-591531460223.us-central1.run.app/api/products"
    
    // MARK: - Propiedades del Carrito
    var cartItems: [CartItem] = []
    
    var totalPrice: Double {
        cartItems.reduce(0) { $0 + ($1.product.price * Double($1.quantity)) }
    }
    
    // MARK: - Propiedades de Favoritos
    private let favoritesKey = "myFavoriteProductIDs" // Llave para guardar
    var favoriteProductIDs: Set<Int> = []
    
    // MARK: - Propiedades del Toast (Mensaje emergente)
    var showToast: Bool = false
    var toastMessage: String = ""

    
    // MARK: - Inicializador
    
    init() {
        // Carga los favoritos guardados en el dispositivo al iniciar la app
        loadFavorites()
    }

Properties

  • products: An array of Product objects fetched from the API. This is where your list of available items is stored, ready to be displayed and managed by your app. The products displayed to the user. Products are at the heart of any e-commerce or shopping application, and this array is the primary source of truth for what's available. This list will contain all the necessary information about each product, such as its name, description, price, and any other relevant details. Managing the products array efficiently is crucial for the overall performance and user experience of the app.

  • isLoading: A boolean flag indicating whether the data is currently being loaded from the API. This is used to show a loading indicator in the UI while the app is fetching the latest product data. Proper management of the isLoading flag can greatly enhance the user experience. When isLoading is set to true, the app can display a spinner or a progress bar to let the user know that something is happening in the background. Once the data has been successfully loaded, isLoading is set back to false, and the UI is updated with the new information. This feedback mechanism prevents the user from thinking that the app is unresponsive during data fetching operations.

  • errorMessage: An optional string to store any error messages that occur during the API request. This allows the app to display user-friendly error messages when something goes wrong. Having a well-defined error handling strategy is essential for any application that relies on external data sources. When an error occurs, the errorMessage property is set with a descriptive message that the app can display to the user. This message should provide enough information for the user to understand what went wrong and, if possible, suggest a course of action. By providing clear and informative error messages, you can help users troubleshoot issues and prevent frustration.

  • apiURL: A private constant storing the URL of the API endpoint. This URL is where the app fetches the product information from. Keeping the API URL as a constant ensures that it is consistent throughout the app and reduces the risk of errors caused by typos or inconsistencies. It also makes it easier to update the API endpoint in the future, as you only need to change it in one place. When constructing the apiURL, it is important to ensure that it is valid and points to the correct location. Using a properly formatted URL is crucial for the app to be able to communicate with the API and retrieve the necessary data.

  • cartItems: An array of CartItem objects representing the items in the shopping cart. The cartItems array is where the app stores the user's selected products along with their quantities. Managing this array efficiently is crucial for providing a smooth and responsive shopping experience. When the user adds a product to the cart, a new CartItem object is created and added to the array. If the user changes the quantity of a product in the cart, the corresponding CartItem object is updated. When the user removes a product from the cart, the corresponding CartItem object is removed from the array. Proper management of the cartItems array ensures that the shopping cart accurately reflects the user's selections.

  • totalPrice: A computed property that calculates the total price of all items in the cart. This property is updated automatically whenever the contents of the cart change. Calculating the totalPrice dynamically ensures that the user always sees the correct total amount, even as they add, remove, or modify items in the cart. This is achieved by iterating over the cartItems array and summing the price of each product multiplied by its quantity. The result is then returned as the totalPrice. By keeping this property up-to-date, the app can provide a seamless and accurate shopping experience for the user.

  • favoritesKey: A private constant storing the key used to save the favorite product IDs in UserDefaults. This key is used to uniquely identify the saved favorites in the user's device. Storing this key as a constant ensures consistency throughout the app and reduces the risk of errors caused by typos or inconsistencies. When saving the favorite product IDs, the app uses this key to store them in UserDefaults. When loading the favorite product IDs, the app uses the same key to retrieve them from UserDefaults. By using a consistent key, the app can ensure that the saved favorites are always accessible and that there are no conflicts with other data stored in UserDefaults.

  • favoriteProductIDs: A set of integers representing the IDs of the user's favorite products. Using a set ensures that there are no duplicate IDs and provides efficient lookup when checking if a product is a favorite. Storing the favorite product IDs in a set allows for quick and easy checking of whether a particular product is a favorite. When the user marks a product as a favorite, its ID is added to the set. When the user unmarks a product as a favorite, its ID is removed from the set. By using a set, the app can efficiently determine whether a product is a favorite without having to iterate over a list or array.

  • showToast: A boolean flag indicating whether a toast message should be displayed. This is used to show temporary messages to the user, such as when a product is added to favorites. Managing the showToast flag is crucial for displaying toast messages at the appropriate times. When showToast is set to true, the app displays a toast message to the user. After a short period of time, the app sets showToast back to false, which causes the toast message to disappear. This mechanism allows the app to provide feedback to the user without interrupting their workflow.

  • toastMessage: A string storing the message to be displayed in the toast. This message provides information to the user about the action that was performed, such as adding a product to favorites. Setting the toastMessage property is essential for displaying informative messages to the user. When the app wants to show a toast message, it sets the toastMessage property with the text that should be displayed. The app then sets the showToast flag to true, which causes the toast message to appear on the screen. By providing clear and concise messages, the app can help the user understand what is happening and provide feedback on their actions.

Initializer

The init() method is used to initialize the ShopViewModel. It calls the loadFavorites() method to load any saved favorite product IDs from UserDefaults when the app starts.

    init() {
        // Carga los favoritos guardados en el dispositivo al iniciar la app
        loadFavorites()
    }

Cart Methods

The ShopViewModel includes methods for managing the shopping cart:

  • addToCart(product: Product, quantity: Int): Adds a product to the cart or updates the quantity if the product is already in the cart.
  • updateQuantity(item: CartItem, newQuantity: Int): Updates the quantity of an item in the cart. If the new quantity is zero or less, the item is removed from the cart.

Favorite Methods with Persistence and Logs

The ShopViewModel provides methods for managing user favorites, including persistence and logging:

  • isFavorite(product: Product) -> Bool: Checks if a product is a favorite.
  • toggleFavorite(product: Product): Toggles the favorite status of a product. This method also shows a toast message and logs the action to the console.
  • getFavoriteProducts() -> [Product]: Returns an array of the user's favorite products.

Saving and Loading Favorites

The saveFavorites() and loadFavorites() methods handle the persistence of favorite product IDs using UserDefaults.

    private func saveFavorites() {
        // Convertimos el Set<Int> a un Array [Int] para poder guardarlo
        let idArray = Array(self.favoriteProductIDs)
        UserDefaults.standard.set(idArray, forKey: favoritesKey)
        print("💾 Favoritos guardados en el dispositivo.")
    }
    
    private func loadFavorites() {
        // Leemos el Array [Int] desde UserDefaults
        let savedIDs = UserDefaults.standard.array(forKey: favoritesKey) as? [Int] ?? []
        // Lo convertimos de nuevo a un Set
        self.favoriteProductIDs = Set(savedIDs)
        print("✅ Favoritos cargados: \(self.favoriteProductIDs.count) IDs encontrados.")
    }

Toast Methods

The ShopViewModel includes methods for displaying toast messages:

  • showToast(message: String): Shows a toast message with the given text. The message is displayed for a short period of time and then automatically dismissed.

Data Loading (API)

The fetchProducts() method is responsible for fetching product data from the API.

    func fetchProducts() async {
        guard !isLoading else { return }
        
        print("Empezando a cargar productos...")
        isLoading = true
        errorMessage = nil
        
        guard let url = URL(string: apiURL) else {
            errorMessage = "URL inválida"
            isLoading = false
            print("Error: URL inválida")
            return
        }

        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            let decoder = JSONDecoder()
            
            let apiResponse = try decoder.decode(APIResponse.self, from: data)
            
            self.products = apiResponse.results
            
            self.isLoading = false
            print("¡Productos cargados exitosamente! Se encontraron \(apiResponse.count) productos.")
            
        } catch {
            errorMessage = "Error al obtener los datos: \(error.localizedDescription)"
            isLoading = false
            print("Error al decodificar: \(error)")
        }
    }

This method performs the following steps:

  1. Checks if data is already loading to prevent multiple concurrent requests.
  2. Sets the isLoading flag to true and resets the errorMessage.
  3. Creates a URL from the apiURL string.
  4. Fetches data from the API using URLSession.shared.data(from: url).
  5. Decodes the JSON response into an APIResponse object.
  6. Assigns the results to the products array.
  7. Sets the isLoading flag to false.
  8. Handles any errors that occur during the process.

Conclusion

The ShopViewModel class provides a comprehensive solution for managing products, cart items, and user favorites in a SwiftUI application. By leveraging the @Observable macro and UserDefaults for persistence, this class enables developers to create a seamless and engaging shopping experience. You can explore more about data persistence in Swift on the Apple Developer Documentation.