Patterns: Commands
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:
Navigation logic
UI updates
Presentation logic
Business logic
Network requests
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:
Keeps the view controller or SwiftUI view clean.
Follows the Single Responsibility Principle (SRP). 🚀
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.
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]) -> Void)
var onError: ((BusinessError) -> Void)
init(…) { … }
func fetchPaymentMethods() async {
let result = await FetchPaymentMethodsCommand().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]) -> Void)
var onError: ((BusinessError) -> 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 FetchPaymentMethodsCommand().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)
}
.onAppear {
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.
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! 🚀
Subscribe to my newsletter
Read articles from Ahmed Ramy directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Ahmed Ramy
Ahmed Ramy
I am an iOS Developer from Egypt, Cairo who likes Apple Products and UX studies I enjoy reading about Entreprenuership and Scrum. When no one is looking, I like to play around with Figma and build tools/ideas to help me during get things done quickly. And I keep repeating epic gaming moments over and over again in my head and repeat narratives out loud in my room to regain a glimpse of how it felt when I first saw it (without altering the script)