Mastering the Singleton Pattern with SwiftMart: Building a Robust Cart System

Arpit Agarwal
TheSwiftGenius
Published in
6 min readJul 20, 2024

--

In the world of iOS development, managing shared resources efficiently is crucial for creating smooth, responsive apps. Enter the Singleton pattern — a powerful tool in our design pattern toolkit that ensures a class has only one instance and provides a global point of access to it. Today, we’re kicking off our design pattern series by implementing a Singleton for the cart system in our SwiftMart app.

Why Singleton for a Cart?

Imagine you’re building an e-commerce app like SwiftMart. Users should be able to add items to their cart from any screen, view their cart contents anytime, and have their cart persist throughout their shopping session. This screams for a globally accessible, single source of truth for the cart’s state. That’s where the Singleton pattern shines.

What We’ll Cover

In this post, we’ll:
1. Explore why a Singleton is perfect for our SwiftMart cart
2. Implement a thread-safe CartManager singleton in Swift
3. Discuss the benefits and potential pitfalls of this approach
4. Test our implementation
5. Look at real-world applications of Singleton in iOS development

Whether you’re a beginner looking to understand your first design pattern or an experienced developer refreshing your skills, this practical guide will help you master the Singleton pattern in a real-world context.

Let’s dive in and start building a robust cart system for SwiftMart!

The Problem: Managing Cart State Across SwiftMart

Before we dive into the Singleton pattern, let’s consider the challenges we face when managing a cart in an e-commerce app like SwiftMart.

Scenario: Multiple Cart Instances

Imagine we’ve created a simple `Cart` class:

class Cart {
var items: [Item] = []

func addItem(_ item: Item) {
items.append(item)
}

func removeItem(_ item: Item) {
items.removeAll { $0.id == item.id }
}

var total: Double {
items.reduce(0) { $0 + $1.price }
}
}

Now, let’s see what happens if we create multiple instances of this `Cart` across different parts of our app:

// In ProductListViewController
let productListCart = Cart()
productListCart.addItem(Item(id: 1, name: “Swift T-Shirt”, price: 25.99))
// In ProductDetailViewController
let productDetailCart = Cart()
productDetailCart.addItem(Item(id: 2, name: “iOS Mug”, price: 15.99))
// In CartViewController
let cartViewCart = Cart()
print(cartViewCart.items.count) // Output: 0
print(cartViewCart.total) // Output: 0.0

The Issues

1. Data Inconsistency: Each view controller has its own `Cart` instance. Adding items in one doesn’t reflect in the others. The user sees an empty cart in `CartViewController`, even though they’ve added items elsewhere.

2. Memory Inefficiency: We’re creating multiple instances of `Cart`, each taking up memory, when we really only need one for the entire app.

3. Lack of Global Access: There’s no easy way for any part of the app to access the current state of the cart without passing the `Cart` instance around, which can lead to complex and tightly coupled code.

4. Potential for Bugs: With multiple cart instances, we risk operations being performed on the wrong instance, leading to lost items or incorrect totals.

5. Difficulty in Persistence: If we want to save the cart state (e.g., when the app goes to the background), we’d need to ensure we’re saving the correct instance, which becomes tricky with multiple carts.

The Need for a Single, Shared Cart

What we really need is:

- A single instance of `Cart` that’s shared across the entire app
- Global accessibility so any part of the app can easily access and modify the cart
- Guarantee that we’re always working with the same cart instance

This is exactly what the Singleton pattern provides. By implementing our `Cart` (or more accurately, a `CartManager`) as a Singleton, we ensure that all parts of our app are working with the same cart data, solving the issues of inconsistency, inefficiency, and accessibility.

The Solution: Singleton Pattern

The Singleton pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to that instance.

Here’s how it addresses our cart management problems:

  1. Single Instance: The Singleton pattern guarantees that only one instance of the `CartManager` exists throughout the app’s lifecycle.
    2. Global Access: It provides a static method to access the single instance from anywhere in the app.
    3. Lazy Initialization: The instance is created only when it’s first needed, conserving resources.

Let’s implement a `CartManager` singleton for SwiftMart:

class CartManager {
static let shared = CartManager()

private init() {}

private var items: [Item] = []

func addItem(_ item: Item) {
items.append(item)
}

func removeItem(_ item: Item) {
items.removeAll { $0.id == item.id }
}

var total: Double {
items.reduce(0) { $0 + $1.price }
}

var itemCount: Int {
items.count
}
}

Let’s break down this implementation:

1. `static let shared = CartManager()`: This creates the single instance of `CartManager`.
2. `private init() {}`: This prevents other parts of the app from creating new instances.
3. The rest of the class contains our cart functionality.

Now, let’s see how we use this in our app:


// In ProductListViewController
CartManager.shared.addItem(Item(id: 1, name: “Swift T-Shirt”, price: 25.99))
// In ProductDetailViewController
CartManager.shared.addItem(Item(id: 2, name: “iOS Mug”, price: 15.99))
// In CartViewController
print(CartManager.shared.itemCount) // Output: 2
print(CartManager.shared.total) // Output: 41.98

As you can see, we’re now working with a single cart instance across the entire app, solving our previous issues.

Benefits and Considerations

Benefits:

1. Guaranteed Single Instance: We ensure only one cart exists in the app.
2. Global Access: Any part of the app can easily access the cart.
3. Lazy Initialization: The cart is only created when first accessed, saving resources.

Considerations:

1. Testing: Singletons can make unit testing more challenging, as they maintain global state.
2. Tight Coupling: Overuse of singletons can lead to tight coupling in your codebase.
3. Concurrency: In a multi-threaded environment, you need to ensure thread-safety.

Testing the CartManager Singleton

Here’s how we might write some unit tests for our `CartManager`:


import XCTest
@testable import SwiftMart

class CartManagerTests: XCTestCase {

override func setUp() {
super.setUp()
CartManager.shared.removeAllItems() // Assuming we’ve added this method
}

func testAddItem() {
let item = Item(id: 1, name: “Test Item”, price: 10.0)
CartManager.shared.addItem(item)
XCTAssertEqual(CartManager.shared.itemCount, 1)
}

func testRemoveItem() {
let item = Item(id: 1, name: “Test Item”, price: 10.0)
CartManager.shared.addItem(item)
CartManager.shared.removeItem(item)
XCTAssertEqual(CartManager.shared.itemCount, 0)
}

func testTotal() {
let item1 = Item(id: 1, name: “Item 1”, price: 10.0)
let item2 = Item(id: 2, name: “Item 2”, price: 20.0)
CartManager.shared.addItem(item1)
CartManager.shared.addItem(item2)
XCTAssertEqual(CartManager.shared.total, 30.0)
}
}

Real-world Applications

The Singleton pattern is widely used in iOS development, often for managing global state or providing a single point of access to a shared resource. Some examples include:

1. `UserDefaults.standard`: For accessing and setting user preferences.
2. `FileManager.default`: For performing file and directory operations.
3. `URLSession.shared`: For making network requests.
4. `NotificationCenter.default`: For broadcasting and observing notifications.

Conclusion

The Singleton pattern provides an elegant solution for managing shared resources like our SwiftMart cart. By ensuring a single, globally accessible instance, we’ve solved issues of data consistency, memory efficiency, and ease of access.

However, remember that with great power comes great responsibility. While Singletons are useful, be cautious not to overuse them. Consider whether a particular use case truly requires a Singleton, or if dependency injection or other patterns might be more appropriate.

In our next post, we’ll explore another design pattern and see how it fits into our SwiftMart app. Stay tuned!

Your Turn

Now that you understand the Singleton pattern, try implementing it in your own projects. Here are some ideas:

1. Create a `UserManager` singleton to handle user authentication and profile information.
2. Implement a `ThemeManager` singleton to manage app-wide visual settings.
3. Build a `LogManager` singleton for centralized logging in your app.

Share your implementations or any questions in the comments below. Happy coding!

--

--