Patterns: Command + Siri


Yes, it’s me again. I know you’re shocked, so am I. This post totally breaks my record of ‘one-and-done’ blog posts. 🤷♂️
I’m seriously surprised I’ve followed through. But hey, whenever the magical words Software Design, Optimization, Time-saving, and Automation get thrown into the mix, my gears start turning. We’re about to merge everything we love—Command Patterns, reuse, composability, and… Siri—for a healthy dose of engineering craziness.
In our previous post on the Command Pattern, we explored how the pattern looks in both MVC and MVVM, using UIKit and SwiftUI. We discussed how it helps us avoid the dreaded “Massive View Controller,” and how easy it is to shape everything like Legos to create reusable, composable building blocks. Perfect for containing your precious business logic in a tidy package.
Siri as a New UI (Looking at It from the Right Angle)
Today, we’re highlighting how Siri can become a new UI if you think about it from the right angle. With iOS 16, Siri can be managed via AppIntents—more flexible than ever—and the same code can also be used with the Shortcuts app.
If you were to say, “Hey Siri, use my app to fuel my car,” you could hook into your internal business logic that’s already wrapped inside a Command Pattern. That means Siri is basically another entry point to a well-structured domain. Let’s see how.
AppShortcutsProvider: Teaching Siri What to listen to
Below is a snippet of how you can register shortcuts with Siri. You create an AppShortcutsProvider
, define the phrases users might say, and supply an Intent
for Siri to call.
@available(iOS 16.0, *)
class SiriShortcutsProvider: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: OrderFuelIntent(),
phrases: [
"Fuel my car",
"Fuel my car at \(.$location)"
],
shortTitle: "Order Fuel",
systemImageName: "fuelpump"
)
}
}
OrderFuelIntent
struct OrderFuelIntent: AppIntent {
// ...
static var title: LocalizedStringResource = "Order Fuel"
static var description = IntentDescription(
"Quickly make a Fuel Order with Previously Used Vehicles and Fuel Settings based on latest Order at Home or Work",
categoryName: "Fuel Ordering",
searchKeywords: ["fuel", "order"]
)
@Parameter(
title: "Location",
requestValueDialog: "Where should the order be?",
optionsProvider: SiriIntentLocationOptionsProvider()
)
var location: Location
// ...
}
Here, location
is a parameter that Siri will prompt for when needed. Once you’ve set up the necessary data, your perform
function is where the magic happens.
Adding the Location Enum
Below, you can see how we define the Location
enum so Siri knows what “Home” or “Work” actually means in your domain model:
@available(iOS 16.0, *)
enum Location: String, CaseIterable {
case home = "Home"
case work = "Work"
}
// MARK: AppEnum
@available(iOS 16.0, *)
extension Location: AppEnum {
public static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(name: "Order Location")
}
public static var caseDisplayRepresentations: [Location: DisplayRepresentation] {
[
.home: DisplayRepresentation(title: "Home"),
.work: DisplayRepresentation(title: "Work"),
]
}
}
@available(iOS 16.0, *)
extension Location {
func mapToDomainModel() -> AddressType {
switch self {
case .home:
return .home
case .work:
return .work
}
}
}
mapToDomainModel
, that’s because by isolating 3rd party entities and wrap them into our own, makes us agnostic of their changes, we covered it in more details while discussing Apple Pay during Error HandlingCloser look at the perform()
function
Wait a minute…, when you look at the perform function
func perform() async throws -> some IntentResult & ProvidesDialog {
// Logic Execution here
}
It doesn’t have any parameters, it executes logic based on the struct’s parameters… this looks familiar
This looks like our AsyncUsecase
from previously on Patterns!
struct FetchPaymentMethodsUsecase: 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)
}
}
When you call execute()
, this use case already knows what to do. Siri’s perform()
method is basically the same structure—it even returns a type that’s constrained to IntentResult
.
Putting It All Together with Siri
When you think about it this way… perform can be a gathering ground for all the usecases to formulate new functionality!
func perform() async throws -> some IntentResult & ProvidesDialog {
// Step 1: Update session with the selected delivery address
await UpdateUserAddressUsecase(newAddress: location.asDomainModel())
.execute()
// Step 2: Fetch the fuel type for the selected vehicle (98, 95, Diesel, or EV)
let fuelType = try await GetFuelTypeForVehicleUsecase()
.execute()
.mapError { SiriIntentError(reason: $0.errorMessage).whileLogging() }
.get()
// Step 3: Fetch the selected vehicle
// session is a means of Dependency Injection to mark user's selected preferences
guard let vehicle = session.selectedVehicles.first else {
throw SiriIntentError(reason: "Couldn't find your selected vehicle")
}
// Step 4: Fetch the payment method
guard let paymentMethod = payment.session.availablePaymentMethods
.first(where: { $0 is SiriPayable }) else {
throw SiriIntentError(reason: "Couldn't find a Siri compatible method to pay with")
}
// Step 5: Set up the order with a full tank of fuel
let fuelQuantity: Quantity = .fullTank
let product = CAFUProduct(fuel: fuelType, quantity: fuelQuantity)
session.order.products = [product]
// Step 6: Get the nearest available time slot for fuel delivery
let timeslot = try await NearestTimeSlotUseCase(variantId: fuelType.variantId)
.execute()
.mapError { SiriIntentError(reason: $0.errorMessage).whileLogging() }
.get()
product.timeSlot = timeslot
payment.select(paymentMethod: paymentMethod)
// Step 7: Create a new order
try await CreateOrderUsecase()
.execute()
// Step 8: Display to the user the order summary
try await requestConfirmation(
result: .result(
view: OrderSummaryView()
),
confirmationActionName: .order,
showPrompt: false
)
// Step 9: Place the order after confirmation
try await PlaceOrderUsecase()
.execute()
// Return success message
let confirmationMessage = "Awesome! Your fuel order has been placed."
return .result(dialog: IntentDialog(stringLiteral: confirmationMessage))
}
Notable Observations
We rely on multiple use cases (Commands) such as
UpdateUserAddress
,GetFuelTypeForVehicleUsecase
, etc.Each use case has a single
execute()
method which encapsulates a distinct piece of business logic.Siri calls
perform()
, which, in turn, calls our chain of commands. This is the definition of modular and reusable code.
How does it look like?
Now About Testing 🧪
One of the biggest advantages of using the Command Pattern (and a clean architecture approach) is how testable it all becomes. Here’s how you can tackle testing at various levels:
1. Testing Each Use Case
Unit Tests: For each use case (e.g.,
GetFuelTypeForVehicleUsecase
,UpdateUserAddressUsecase
), you can write straightforward unit tests that verify the business logic in isolation.Mock Dependencies: If your use case depends on a repository or a network service, inject a mock or fake. This ensures you test the logic without hitting a real backend.
Error Cases: Test how each use case handles different failure scenarios (e.g., “No internet,” “Invalid vehicle ID,” etc.).
func testGetFuelTypeForVehicle() async throws {
// Given
let vehicle = Vehicle.stub(...)
let sut = GetFuelTypeForVehicleUsecase(vehicle: vehicle, repository: MockFuelRepository())
// When
let result = await sut.execute()
// Then
switch result {
case .success(let fuelType):
XCTAssertEqual(fuelType, .gas98)
case .failure(let error):
XCTFail("Expected success, got \(error) instead")
}
}
2. Testing the Siri Integration
Integration Tests: Since Siri’s
perform()
acts as an entry point that orchestrates multiple use cases, you can run an integration test to confirm that everything plays nicely together.Mock the Siri Environment: Apple doesn’t provide a direct unit test harness for SiriKit or AppIntents in Xcode. However, you can abstract away the domain logic from the Siri layer. Then, test the domain logic in a normal Xcode test target.
Automated UI Testing (Optional): If you want to test the voice-driven flow, you might consider an automated test setup (like UI tests), but that can get tricky. Usually, verifying your domain logic + making sure your Siri Intents are valid with the “Shortcuts” integration is enough.
3. Testing the Flow with Dependency Injection
Dependency Injection: Notice in the example code, we keep referencing
session
orpayment
. In production, these might be singletons or injected services. For tests, you can provide test doubles—likeTestSession
orMockPaymentService
—to control the data.Verifying Side Effects: If
CreateOrderUsecase
triggers a network call or updates a database, you can check your mock objects to see if the correct methods were called with the right parameters.
Conclusion
The Command Pattern is powerful. By encapsulating each piece of business logic into its own “command,” we avoid cluttering our UI or Siri integration with domain knowledge. Siri (thanks to AppIntents) is just another consumer of our well-structured domain. That means:
Easier maintenance: We only need to manage business logic in one place.
Better testing: Each Command/Usecase can be tested independently.
Less code duplication: The same commands are called whether you’re interacting via the app’s UI or Siri.
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)