Patterns: Commands

Patterns: Commands

Breaking the Chains of Massive View Controllers: Lessons for SwiftUI and Beyond

Hello, world! 👋 It’s been a while since I’ve shared something with you all, and today feels like the perfect day to dive into an old, yet very relevant, topic. Let’s talk about a classic architectural debate in iOS development and how its lessons can help us avoid pitfalls with SwiftUI.


The Problem: Massive View Controllers (MVC’s Evil Twin)

Before 2019, many of us were deep in the trenches of the infamous Architecture Wars, battling over patterns like MVC, MVVM, and VIPER. At the heart of it all was the struggle to properly separate concerns in our codebase.

We weren’t mad at MVC itself; we were frustrated by how easily it evolved into Massive View Controllers.

Let’s break it down. The humble UIViewController often became an all-you-can-eat buffet of logic, responsible for tasks like:

  1. Navigation logic

  2. UI updates

  3. Presentation logic

  4. Business logic

  5. Network requests

  6. Caching mechanisms

Before we knew it, our view controllers ballooned to over 1,000 lines of code. 🥲


Was It Really MVC’s Fault?

Nope! As Dave delves into brilliantly in this article, the issue wasn’t the MVC pattern itself. It was our interpretation of where things should live.

Apple always recommended breaking things down:

  • Models: Split them into smaller reusable units.

  • View Controllers: Divide and conquer! Use child view controllers.

  • Views: Decompose complex layouts into smaller, reusable pieces.

With SwiftUI, Apple continues to encourage breaking things down further, promoting better modularity.

When we follow that, we avoid falling into the same rabbit-hole over and over again

But today, I want to focus on tackling logic, or the "M" in any MVx pattern. Enter: Command Pattern.


The Command Pattern: A Fresh Perspective

At its core, the Command Pattern is about encapsulating logic into small, focused units. Think of it as a class (or struct) with a single responsibility: an execute() method.

Here’s an example:

struct FetchPaymentMethods: AsyncUsecase { 
    func execute() async -> Result<[PaymentMethod], BusinessError> { 
        // 1. Fetch payment methods
        // 2. Fetch wallet
        // 3. Preselect a favorite
        // 4. Disable unsupported methods
        // 5. Add Apple Pay (if supported)
    }
}

This does a few things immediately:

  1. Keeps the view controller or SwiftUI view clean.

  2. Follows the Single Responsibility Principle (SRP). 🚀

  3. Improves readability and maintainability.

If this logic grows too complex, you can further break it down into smaller commands:

struct FetchPaymentMethods: AsyncUsecase { 
    func execute() async -> Result<[PaymentMethod], BusinessError> { 
        await [
            FetchWallet(),
            PreselectFavorite(),
            DisableUnsupportedMethods(),
            AddApplePayIfSupported()
        ].asyncForEach { await $0.execute() }
    }
}

Why Use the Command Pattern?

Modular Expansion: Need to add a new step? No problem—just add a new command. Old implementations remain untouched.

🥳
Achievement Unlocked: Open-Closed Principle (OCP). 🚀

Composability: Combine multiple commands to create complex workflows. For instance, placing an order might look like this:

struct PlaceOrder: AsyncUsecase {
    func execute() async -> Result<Void, BusinessError> {
        await [
            ApplyPromoCode(),
            UpdatePaymentMethod(),
            ValidateCheckout()
        ].asyncForEach { await $0.execute() }
    }
}

Testability: Each command is a small, isolated unit—perfect for writing focused unit tests.

Reusability: Commands are decoupled from the UI, making them reusable across different platforms, whether you’re building for iOS, macOS, or even CLI apps. 😄


A Word on SwiftUI

While this pattern shines in UIKit, it’s equally valuable in SwiftUI. With SwiftUI’s declarative nature, logic can creep into views if you’re not careful. Using the Command Pattern ensures your business logic remains in its rightful place—clean, testable, and reusable.

Example Time

So, now that we’re almost done, let’s give some examples about how can we make this possible

UIKit: Where the Model is the one in Command

In MVC, the Model is responsible for managing the app’s data. By incorporating the Command Pattern, we can offload specific business logic into dedicated commands, keeping the Model lean and maintainable.

// Command to encapsulate the business logic
struct FetchPaymentMethodsUsecase: AsyncUsecase {
    func execute() async -> Result<[PaymentMethod], BusinessError> {
        // 1. Fetch payment methods
        // 2. Process wallet logic
        // 3. Preselect favorite payment method
        // 4. Disable unsupported methods
        // 5. Add Apple Pay if available
    }
}

// Model using the command
final class PaymentModel {
    private var paymentMethods: [PaymentMethod] = []
    private var error: BusinessError?
    // Model communicates his output via Callbacks
    // This acts as a contract of communication and what to expect from the Model
    // Defining this is super important to reason about, as it defines how we can test our models
    var onPaymentMethodsUpdate: (([PaymentMethod]) @MainActor @Sendable -> Void) 
    var onError: ((BusinessError) @MainActor @Sendable -> Void) 

    init(…) { … }

    func fetchPaymentMethods() async {
        let result = await FetchPaymentMethodsUsecase().execute()
        // Further Business Logic may happen here, 
        // like composing different Usecases together
    }
}

// ViewController (Controller in MVC)
class PaymentViewController: UIViewController {
    private let paymentModel: PaymentModel

    init(…) { 
       paymentModel = init(onPaymentMethodsUpdate: {…}, onError: {…})
       super.init(…)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        Task { await paymentModel.fetchPaymentMethods() }
    }

    private func displayPaymentMethods() { 
       // Here we can map/translate/render our Business Data Models towards UI representations
    }
}

SwiftUI: Where the Model is still the one in Command

In MVVM, the ViewModel acts as the mediator between the view and model. The Command Pattern fits naturally here, allowing the ViewModel to delegate logic cleanly.

// Command remains the same
struct FetchPaymentMethodsUsecase: AsyncUsecase {
    func execute() async -> Result<[PaymentMethod], BusinessError> {
        // Business logic as before
    }
}

// Model using the command
final class PaymentModel {
    private var paymentMethods: [PaymentMethod] = []
    private var error: BusinessError?
    // Model communicates his output via Callbacks
    // This acts as a contract of communication and what to expect from the Model
    // Defining this is super important to reason about, as it defines how we can test our models
    var onPaymentMethodsUpdate: (([PaymentMethod]) @MainActor @Sendable -> Void) 
    var onError: ((BusinessError) @MainActor @Sendable -> Void) 

    init(…) { … }

    func fetchPaymentMethods() async {
        let result = await FetchPaymentMethodsCommand().execute()
        // Further Business Logic may happen here, 
        // like composing different Usecases together
    }
}

// ViewModel in MVVM
class PaymentViewModel: ObservableObject {
    @Published var paymentMethods: [PaymentMethodUI] = []
    @Published var error: PresentableError?

    private let model: PaymentModel

    init(…) { … }

    func fetchPaymentMethods() async {
        let result = await FetchPaymentMethodsUsecase().execute()
        // Further Business Logic may happen here, 
        // like composing different Usecases together
    }
}

// View in SwiftUI (MVVM)
struct PaymentView: View {
    @StateObject private var viewModel = PaymentViewModel()

    var body: some View {
        List(viewModel.paymentMethods) { method in
            Text(method.title)
        }.task { 
            await viewModel.fetchPaymentMethods()
        }.alert(item: $viewModel.error) { … }
    }
}

Key Takeaways

1. In MVC, the Command Pattern helps encapsulate business logic in the Model, keeping the ViewController clean and focused on its primary responsibilities.

2. In MVVM, the Command Pattern allows the ViewModel to manage logic, enabling clear communication between the View and Model.

By using the Command Pattern with either architecture, you enhance modularity, testability, and maintainability.

🤔
Key Question: Notice that Model is same in both approaches? while in MVC the UI mapping takes place within the ViewController, while in MVVM in takes place within the ViewModel?
💡
Having it as such, decouples the View code from presentation logic, allowing for possibility of reusing it across different UI frameworks (be it UIKit, SwiftUI, macOS, watchOS, heck… even CLI if you want)

Final Thoughts

SwiftUI might feel like a fresh start, but without discipline, the same old traps of massive files and tangled logic await us. By adopting patterns like Command, we can keep our apps clean, maintainable, and a joy to work on.


What do you think? Have you used the Command Pattern in your projects? Let’s discuss in the comments below! 🚀