SwiftUI ShopViewModel: Products, Cart & Favorites!
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 usingUUID.product: TheProductinstance 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 ofProductobjects 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 theproductsarray 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 theisLoadingflag can greatly enhance the user experience. WhenisLoadingis set totrue, 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,isLoadingis set back tofalse, 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, theerrorMessageproperty 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 theapiURL, 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 ofCartItemobjects representing the items in the shopping cart. ThecartItemsarray 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 newCartItemobject is created and added to the array. If the user changes the quantity of a product in the cart, the correspondingCartItemobject is updated. When the user removes a product from the cart, the correspondingCartItemobject is removed from the array. Proper management of thecartItemsarray 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 thetotalPricedynamically 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 thecartItemsarray and summing the price of each product multiplied by its quantity. The result is then returned as thetotalPrice. 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 theshowToastflag is crucial for displaying toast messages at the appropriate times. WhenshowToastis set totrue, the app displays a toast message to the user. After a short period of time, the app setsshowToastback tofalse, 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 thetoastMessageproperty is essential for displaying informative messages to the user. When the app wants to show a toast message, it sets thetoastMessageproperty with the text that should be displayed. The app then sets theshowToastflag totrue, 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:
- Checks if data is already loading to prevent multiple concurrent requests.
- Sets the
isLoadingflag totrueand resets theerrorMessage. - Creates a URL from the
apiURLstring. - Fetches data from the API using
URLSession.shared.data(from: url). - Decodes the JSON response into an
APIResponseobject. - Assigns the results to the
productsarray. - Sets the
isLoadingflag tofalse. - 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.