This article demonstrates how the Command Pattern, with a bit of ‘rethinking’ in Swift, provides all the benefits of this classic pattern with very little of the verbosity and boilerplate code associated with its object-oriented implementation. We explore how the Command Pattern can be applied to value and reference types, and how using Swift value types to build domain models can enable us to create commands, and support undo and redo operations, with a simple and efficient implementation.
Related article:
Contents
1. Intent
2. Structure
3. Benefits
Implementing the Command Pattern in Swift
1. Creating commands for reference types
2. Making commands more flexible with protocol types
3. Creating commands for value types
4. Making commands generic
Case study
1. Modeling the domain
2. Making things configurable
3. Encapsulating actions with commands
4. Creating the view model
5. Wiring up the UI
6. Adding unlimited levels of undo and redo
7. Enhancing the UI
Conclusion
Command Pattern
1. Intent
The Command Pattern enables encapsulation of calls to methods of domain entities using a type with a simple and consistent interface, so these method calls can be executed, queued, logged, and undone, in a uniform manner.
2. Structure
The class diagram below illustrates the structure of the Command pattern:
The Command
type defines an interface, to be used for executing an operation. Each ConcreteCommand
type implements this interface to perform a specific operation on the Receiver. The Client creates instances of ConcreteCommand
types, all of which can be executed by the Invoker using the interface defined by the Command
type.
3. Benefits
The flexibility of the pattern comes from the fact that the Invoker can execute any concrete command using the Command
interface without knowing what action the concrete command performs and how it manipulates the Receiver. Being able to encapsulate actions as instances of a type effectively decouples the code that creates a command from the code that executes it, and from the entity or entities which perform the action. This decoupling can manifest itself in the following ways:
- Functional decoupling: The code that executes an action does not need to know how the action is performed.
- Source decoupling: The code that executes an action is not concerned with how the action was created and who created it.
- Timing decoupling: Converting actions into instances of a type that can be stored for later execution allows the application to create actions when it is convenient to do so, and execute them whenever and as many times as required.
Implementing the Command Pattern in Swift
Swift is a multi-paradigm programming language, offering a rich collection not only of reference types, in the form of classes and closures, but also of value types, in the form of structs, enums and tuples. Swift functions are first-class types so they can be assigned to variables, passed into functions as arguments, and returned from functions. Function types can thus be used to encapsulate actions using a convenient and light-weight syntax. This lets us accomplish what the Command Pattern does, without a plethora of types and boilerplate code.
1. Creating commands for reference types
Let’s say we have two entities in our application that perform certain actions. Let’s call them EntityOne
and EntityTwo
.
class EntityOne {
func performEntityOneAction() {
print("Entity one action performed")
}
}
class EntityTwo {
func performEntityTwoAction() {
print("Entity two action performed")
}
}
We can use the Command Pattern to get instances of the above types to perform their respective actions, without requiring the unit of code that will execute the command (the Invoker) to know which entity is the Receiver of the command, and what action it will perform when the command is executed. Unlike the object-oriented implementation of the Command Pattern, however, where a separate concrete type must be defined for each action we want the Receiver to perform, with all the concrete types conforming to an abstract Command
type, with Swift we just need a single concrete Command
type, which can be given the action to perform as a closure with no parameters and no return value. The closure is passed in through the initializer and stored. When the command is executed, it invokes the closure to perform the action, as shown below.
class Command {
typealias Action = () -> Void
private let action: Action
init(_ action: @escaping Action) {
self.action = action
}
func execute() {
action()
}
}
We can create instances of the Command
type that get instances of EntityOne
and EntityTwo
to perform their respective actions.
var entityOne = EntityOne()
var entityTwo = EntityTwo()
let entityOneCommand = Command() { [weak entityOne] in
entityOne?.performEntityOneAction()
}
let entityTwoCommand = Command() { [weak entityTwo] in
entityTwo?.performEntityTwoAction()
}
The request to the Receiver to perform its action is encapsulated in the closure. Note that the commands do not need to store references to their respective Receivers because these are implicitly captured by the closures. We can execute each action separately, or store them in an array or other collection and execute them together.
entityOneCommand.execute()
// Entity one action performed
entityTwoCommand.execute()
// Entity two action performed
let commands = [entityOneCommand, entityTwoCommand]
for command in commands {
command.execute()
}
// Entity one action performed
// Entity two action performed
In the above example, we have given the closures weak references to the Receivers, using optional chaining to call the relevant methods. This means that if all other references to a given Receiver cease to exist, the closure will not stop the Receiver from getting deallocated whereupon the command will simply stop working. This ensures that a Receiver which is no longer relevant to the application does not go on living just because of a strong reference captured by a command. We show this below by giving the entityOne
variable a reference to a new EntityOne
instance, which causes the instance we passed to entityOneCommand
to be deallocated, and the reference to the instance held by the closure gets set to nil
. If we subsequently attempt to execute the command, the optional chain used to invoke the action fails gracefully, leading to the action not being performed but also no error or crash.
entityOne = EntityOne()
entityOneCommand.execute()
entityTwoCommand.execute()
// Entity two action performed
There could be cases, however, where we want a command to continue to work even if no other reference to its Receiver exists. It is also possible that the closure passed to the command is meant to hold the only reference to its Receiver. In such cases, we can give the closure a strong reference to the Receiver. This way, even if no other reference to the Receiver exists, the strong reference held by the closure will prevent the Receiver from getting deallocated. We show this below by creating a command with a strong reference to an instance of EntityTwo
, which holds on to its reference and prevents the instance from getting deallocated when we assign the original entityTwo
variable a reference to a new instance of EntityTwo
.
let entityTwoCommandWithStrongReference = Command() {
entityTwo.performEntityTwoAction()
}
entityTwo = EntityTwo()
entityTwoCommandWithStrongReference.execute()
// Entity two action performed
It is advisable to be careful when using strong references with closures, for two reasons. First, this may lead to unexpected results since it can cause an entity that may have been replaced for all other purposes to remain alive because of a closure having captured a strong reference to it, which may not be the intended outcome. Second, if care is not taken when creating closures with strong references, strong reference cycles can be created, leading to memory leaks.
2. Making commands more flexible with protocol types
So far, we have shown commands that capture references to their Receivers and get bound to them. This is in essence what the Command Pattern does, enabling commands to be executed without requiring the Invoker to have any knowledge about the Receiver which performs the action. Whilst this is useful in certain contexts, there could be cases where we want to give the Invoker the ability to choose the Receiver on which to execute a command. For instance, in a game where different characters can perform the same actions, such as running, jumping, or using a weapon, the actions could be encapsulated by the controller to be used on any character which can perform that action.
To demonstrate this approach, we create two new entities containing action methods with the same signature but different implementations.
class EntityThree {
func performAction() {
print("Action performed by entity three")
}
}
class EntityFour {
func performAction() {
print("Action performed by entity four")
}
}
We create a protocol with the common method signature, and make the two entities conform to the protocol.
protocol ActionProtocol {
func performAction()
}
extension EntityThree: ActionProtocol {}
extension EntityFour: ActionProtocol {}
We need a new command type which will store a closure that takes a single parameter of the ActionProtocol
type with no return value, which is shown below.
class ActionCommand {
typealias Action = (ActionProtocol) -> Void
private let action: Action
init(_ action: @escaping Action) {
self.action = action
}
func execute(with entity: ActionProtocol) {
action(entity)
}
}
Here is an instance of the above type.
let actionCommand = ActionCommand() { $0.performAction() }
Now we can pass instances of EntityThree
and EntityFour
to the command to get these entities to perform the action in their own way.
let entityThree = EntityThree()
let entityFour = EntityFour()
actionCommand.execute(with: entityThree)
// Action performed by entity three
actionCommand.execute(with: entityFour)
// Action performed by entity four
3. Creating commands for value types
The application development paradigm facilitated by Swift enables us to create powerful value types to encapsulate domain data, logic, and business rules. With this approach, most of our application logic is built using value types, and reference types are used mainly where we need to preserve state, either within the application or to connect to user interfaces, persistence mechanisms, networks, etc. The command examples we have seen so far all deal with reference types. Whilst these remain relevant in Swift, most commands we expect to use in Swift applications in practice would manipulate or transform values.
To show how we can create commands to transform instances of value types, we define the following command, which stores a closure with a single parameter of the Int
type and a return value of the same type. When the execute(with:)
method is called, it returns the value obtained by invoking the closure with its argument.
struct IntCommand_nonMutating {
typealias Action = (Int) -> Int
private let action: Action
init(_ action: @escaping Action) {
self.action = action
}
func execute(with value: Int) -> Int {
action(value)
}
}
Here is an instance of the above command that can be used to triple an Int
value.
let tripleCommand_nonMutating = IntCommand_nonMutating() { $0 * 3 }
let number = 4
var tripled = tripleCommand_nonMutating.execute(with: number)
print("Number: \(number), Tripled: \(tripled)")
// Number: 4, Tripled: 12
This command works only for cases where the closure takes an Int
value and returns a value of the same type. A number of actions performed on value types in Swift are mutating actions, which have the effect of mutating the value in place. To handle such cases, we need to implement a new command type as shown below.
struct IntCommand_mutating {
typealias MutatingAction = (inout Int) -> Void
private let mutatingAction: MutatingAction
init(_ mutatingAction: @escaping MutatingAction) {
self.mutatingAction = mutatingAction
}
func execute(with value: Int) -> Int {
var valueToMutate = value
mutatingAction(&valueToMutate)
return valueToMutate
}
}
Now we can use methods of the Int
type which mutate the value in place.
let negateCommand_mutating = IntCommand_mutating() { $0.negate() }
var negated = negateCommand_mutating.execute(with: number)
print("Number: \(number), Negated: \(negated)")
// Number: 4, Negated: -4
The question that naturally arises is can we create a single command type to handle both the above use cases. To do this, we create a unified IntCommand
implementation, where we define initializers for mutating as well as non-mutating actions. The initializer with the mutating action converts the mutating action into a non-mutating action, so a single action can be stored to be invoked when the command is executed.
struct IntCommand {
typealias Action = (Int) -> Int
typealias MutatingAction = (inout Int) -> Void
private let action: Action
init(_ action: @escaping Action) {
self.action = action
}
init(_ mutatingAction: @escaping MutatingAction) {
action = { value in
var valueToMutate = value
mutatingAction(&valueToMutate)
return valueToMutate
}
}
func execute(with value: Int) -> Int {
action(value)
}
}
This command type can handle both mutating and non-mutating actions, as shown below.
let tripleCommand = IntCommand() { $0 * 3 }
let negateCommand = IntCommand() { $0.negate() }
tripled = tripleCommand.execute(with: number)
negated = negateCommand.execute(with: number)
print("Number: \(number), Tripled: \(tripled), Negated: \(negated)")
// Number: 4, Tripled: 12, Negated: -4
Since the final version handles both the cases, we potentially only need that implementation. However, we have worked through all three implementations because there are likely to be scenarios where we need only the mutating or the non-mutating version so we can avoid the extra complexity in trying to provide flexibility that is actually not required. We will see an example of such a use case, where only the mutating version is required, in the case study which we will get to shortly.
4. Making commands generic
The command implementations we have used in the previous section for working with values of the Int
type can be used unchanged for any other value type. Since the actual action to be performed is encapsulated in the closure, the command implementation itself doesn’t depend on the type of the value for which the command is being used. So we could define a generic command type to work with values, which can be specialized at the point of instance creation. This can be done for commands that perform mutating or non-mutating actions. For the purpose of illustration, we show below a generic ValueCommand
type which caters to both mutating and non-mutating actions.
struct ValueCommand<Value> {
typealias Action = (Value) -> Value
typealias MutatingAction = (inout Value) -> Void
private let action: Action
init(_ action: @escaping Action) {
self.action = action
}
init(_ mutatingAction: @escaping MutatingAction) {
action = { value in
var valueToMutate = value
mutatingAction(&valueToMutate)
return valueToMutate
}
}
func execute(with value: Value) -> Value {
action(value)
}
}
Here is how this generic type can be used, for the same use cases that have been shown earlier.
let tripleIntCommand = ValueCommand<Int>() { $0 * 3 }
let negateIntCommand = ValueCommand<Int>() { $0.negate() }
tripled = tripleIntCommand.execute(with: number)
negated = negateIntCommand.execute(with: number)
print("Number: \(number), Tripled: \(tripled), Negated: \(negated)")
// Number: 4, Tripled: 12, Negated: -4
Case study
In the remainder of this article, we will use a small case study to demonstrate using commands with domain models built using value types. We will build a simple calculator which performs the four arithmetic operations. To keep the example simple, we will execute operations in the order in which they are entered, without applying the rules of precedence. Subsequently, we will enhance the calculator to add unlimited levels of undo and redo. The way in which we will design the application, by using commands to perform calculation actions, will make it easy not only to configure the calculator buttons but also to add new features.
1. Modeling the domain
We start by thinking about the kinds of input the calculator with handle, which includes digits from 0
to 9
, the four arithmetic operations, and instructions to evaluate operations and clear the calculator.
To represent the digits 0
to 9
, we create a domain-specific type called Digit
, as shown below.
enum Digit: Int, CustomStringConvertible {
case zero, one, two, three, four, five, six, seven, eight, nine
var description: String {
"\(rawValue)"
}
}
Similarly, we create the following type to represent the four arithmetic operations that we need to support.
enum ArithmeticOperation {
case addition, subtraction, multiplication, division
}
The next step is to model an arithmetic expression that can store and evaluate an arithmetic operation.
struct ArithmeticExpression: Equatable {
var number: Decimal
var operation: ArithmeticOperation
func evaluate(with secondNumber: Decimal) -> Decimal {
switch operation {
case .addition:
return number + secondNumber
case .subtraction:
return number - secondNumber
case .multiplication:
return number * secondNumber
case .division:
return number / secondNumber
}
}
}
Note that ArithmeticExpression
conforms to the Equatable
protocol, which means that any two arithmetic expressions can be compared for equality using the ==
operator. This attribute-based equality is one of the defining characteristics of values. Swift automatically synthesizes Equatable
conformance for structs provided the conformance is declared in the original definition of the struct, or in an extension in the same file, and all stored properties of the struct are of types that conform to Equatable
. In case you are wondering why we did not declare Equatable
conformance for the Digit
and ArithmeticOperation
enums, all Swift enums are automatically Equatable
as long as they don’t have any associated values. If an enum does have one or more associated values, all we have to do is to declare Equatable
conformance in the original definition of the enum, or in an extension in the same file, provided all associated values are of types that conform to Equatable
.
We are now ready to create the type that will enable us to build numbers from digits entered by a user, and to apply arithmetic operations to these numbers to perform calculations.
struct Calculator: Equatable {
private var newNumber: Decimal?
private var expression: ArithmeticExpression?
private var result: Decimal?
var number: Decimal? {
newNumber ?? expression?.number ?? result
}
mutating func setDigit(_ digit: Digit) {
guard !(newNumber == nil && digit == .zero) else { return }
let numberString = newNumber.map(String.init) ?? ""
newNumber = Decimal(string: numberString.appending("\(digit)"))
}
mutating func setOperation(_ operation: ArithmeticOperation) {
guard var numberToUse = newNumber ?? result else { return }
if let existingExpression = expression {
numberToUse = existingExpression.evaluate(with: numberToUse)
}
expression = ArithmeticExpression(number: numberToUse, operation: operation)
newNumber = nil
}
mutating func evaluate() {
guard let numberToUse = newNumber, let expressionToEvaluate = expression else { return }
result = expressionToEvaluate.evaluate(with: numberToUse)
newNumber = nil
expression = nil
}
mutating func allClear() {
newNumber = nil
expression = nil
result = nil
}
}
2. Making things configurable
We need a way to configure the calculator, which includes the button labels and the number of columns used for button layout. To make button configuration flexible, it can be read from an external file or retrieved from user defaults. For this example, we will decode button configuration information from a JSON
file. We save the following file in the main bundle as ‘ButtonConfiguration.json
‘.
{
"labels": [
"7", "8", "9", "÷", "4", "5", "6", "x", "1", "2", "3", "-", "AC", "0", "=", "+"],
"columnCount": 4
}
We now define a ButtonConfiguration
type, which has properties for button labels and button column count, and also has a decoded
property, which decodes a ButtonConfiguration
value from the above JSON
file. Here is the code.
struct ButtonConfiguration: Equatable, Decodable {
let labels: [String]
let columnCount: Int
static let decoded = Bundle.main.decode(
ButtonConfiguration.self, from: "ButtonConfiguration.json")
}
3. Encapsulating actions with commands
Using the Command Pattern will enable us to create commands to control the calculator based on the button configuration information read from the JSON
file, so no change needs to be made to the code when there is a change in the button configuration. It will also make it relatively easier to extend the functionality of the calculator at a later time.
Since all calculation actions performed by a user will mutate a Calculator
instance, we need commands that use a mutating action to mutate a copy of the argument, and return the mutated value. Here is the code for a CalculatorCommand
type that fits the bill.
struct CalculatorCommand {
typealias MutatingAction = (inout Calculator) -> Void
private let mutatingAction: MutatingAction
init(_ mutatingAction: @escaping MutatingAction) {
self.mutatingAction = mutatingAction
}
func execute(with value: Calculator) -> Calculator {
var valueToMutate = value
mutatingAction(&valueToMutate)
return valueToMutate
}
}
The following CommandFactory
can be used to create an instance of CalculatorCommand
corresponding to a string button label.
struct CommandFactory {
private static let arithmeticOperationForLabel: [String: ArithmeticOperation] = [
"+": .addition, "-": .subtraction, "x": .multiplication, "÷": .division]
private static let evaluateLabel = "="
private static let allClearLabel = "AC"
static func createCommandForLabel(_ label: String) -> CalculatorCommand {
if let integer = Int(label), let digit = Digit(rawValue: integer) {
return CalculatorCommand() { $0.setDigit(digit) }
}
if let operation = arithmeticOperationForLabel[label] {
return CalculatorCommand() { $0.setOperation(operation) }
}
if label == evaluateLabel {
return CalculatorCommand() { $0.evaluate() }
}
if label == allClearLabel {
return CalculatorCommand() { $0.allClear() }
}
fatalError("Unable to create command for label: \(label)")
}
}
4. Creating the view model
For this example, we will use a view model, which is part of the Model-View-ViewModel (MVVM) architecture pattern. Although a view model is not required to create applications with SwiftUI, it usually works well to provide a bridge between a domain model built using value types and SwiftUI views, which are also values.
final class CalculatorViewModel: ObservableObject {
@Published private var calculator = Calculator()
private lazy var commands = {
buttonLabels.map(CommandFactory.createCommandForLabel)
}()
let buttonLabels: [String]
let buttonColumnCount: Int
var displayText: String? {
calculator.number.map(String.init)
}
init(buttonConfiguration: ButtonConfiguration = .decoded) {
buttonLabels = buttonConfiguration.labels
buttonColumnCount = buttonConfiguration.columnCount
}
func performAction(forButtonLabelIndex index: Int) {
calculator = commands[index].execute(with: calculator)
}
}
Note that CalculatorViewModel
conforms to the ObservableObject
protocol, which will enable the view to observe certain parts of the view model’s state and update itself accordingly. We can use the @Published
property wrapper to control which parts of the view model’s state, when changed, will trigger the view to be reloaded. We have given this wrapper to the calculator
property, since we want the view to be updated whenever new values are assigned to this property.
The view model initializer takes a parameter of the ButtonConfiguration
type, which is used to initialize the buttonLabels
and buttonColumnCount
properties. The initializer uses the value of the .decoded
property of ButtonConfiguration
as the default value for the buttonConfiguration
parameter so the view model can be initialized without providing any arguments.
The displayText
property returns a string representation of the number
property of the Calculator
instance assigned to the calculator
property. The executeCommand(forIndex:)
method will be used to execute relevant commands to operate the calculator as user input is received.
5. Wiring up the UI
SwiftUI code typically involves applying a number of modifiers to the views we define. Creating custom view modifiers is a nice way to minimize code repetition while ensuring consistent styling of views. It also helps keep the view body
focused on laying out the views, leaving formatting details to the relevant view modifiers.
First we define a view modifier to format the text which will represent the display of the calculator.
struct DisplayText: ViewModifier {
func body(content: Content) -> some View {
content
.font(.largeTitle)
.padding()
.frame(maxWidth: .infinity, alignment: .trailing)
.background(Color(.label))
.foregroundColor(Color(.systemBackground))
.lineLimit(1)
.cornerRadius(5)
}
}
We will also use a view modifier to format the labels of calculator buttons.
struct ButtonLabel: ViewModifier {
func body(content: Content) -> some View {
content
.font(.title2)
.padding()
.frame(maxWidth: .infinity, alignment: .center)
.background(Color(.systemFill))
.foregroundColor(Color(.label))
.cornerRadius(5)
}
}
While we can use the view modifiers as defined above, the following View
extension makes their use easier and more natural at the call site.
extension View {
func displayText() -> some View {
modifier(DisplayText())
}
func buttonLabel() -> some View {
modifier(ButtonLabel())
}
}
Finally, it is time to create our view, which creates and observes an instance of CalculatorViewModel
by means of the @StateObject
property wrapper.
struct ContentView: View {
@StateObject var viewModel = CalculatorViewModel()
var buttonColumns: Array<GridItem> {
Array(repeating: GridItem(.flexible()), count: viewModel.buttonColumnCount)
}
var body: some View {
VStack {
Text(viewModel.displayText ?? " ")
.displayText()
LazyVGrid(columns: buttonColumns) {
ForEach(0..<viewModel.buttonLabels.count, id: \.self) { index in
Button(viewModel.buttonLabels[index]) {
viewModel.performAction(forButtonLabelIndex: index)
}
.buttonLabel()
}
}
}
.padding()
}
}
We now have a functioning calculator, which performs calculations using the basic arithmetic operations, in the order in which the operations are entered. Note that we have not specified any actual colours for the background and foreground of the display and the buttons. Instead, we have used .label
, .systemBackground
, and .systemFill
. These are system colours provided by iOS, which adapt to vibrancy and changes in accessibility settings, and also change appropriately when a user selects light or dark mode on their device.
Here is how the calculator looks in light mode.
When the user selects dark mode, the colours adapt automatically, as shown below.
6. Adding unlimited levels of undo and redo
One of the features of the Command Pattern is to create commands that are able to undo themselves, effectively reversing their effect, without the code that executes and undoes the commands having to know how. With the object-oriented implementation of the Command Pattern, however, quite a bit of code needs to be written to undo commands, and redoing them requires maintaining and manipulating stacks of commands that have been executed and undone, to maintain reversibility.
The complexity comes from the very nature of object-oriented design, where classes are the vehicle for modeling domain entities, and each class is responsible for maintaining its state by directly manipulating scalar values and relatively simple data structures. There is no easy way, therefore, to reconstruct the past state of an entity. To enable a command to know what state the Receiver was in before it was executed, the Command Pattern requires an undoable command to store a snapshot of the state of the Receiver before executing its action. This state snapshot is created by the Receiver and handed off to the command, which enables the command to undo its action, when required, by handing the state snapshot back to the Receiver and asking it to restore itself to the old state. The Memento Pattern, another one of the GoF patterns, is sometimes used to facilitate state capture and restoration without breaking encapsulation.
Swift enables us to entirely sidestep this complexity. With value types used to model the domain, we can store successive states of the model, and step back in time, with relative ease. In our case, the main enhancement we have to make to our earlier design is to define a ValueHistory
type, which can store the history of values of any Equatable
type.
struct ValueHistory<Value: Equatable> {
private var values = [Value]()
private var index = 0
private var lastIndex: Int {
values.count - 1
}
init(_ initialValue: Value) {
values.append(initialValue)
}
var current: Value {
values[index]
}
var hasPrevious: Bool {
index > 0
}
var hasNext: Bool {
index < lastIndex
}
mutating func previous() {
guard hasPrevious else { return }
index -= 1
}
mutating func next() {
guard hasNext else { return }
index += 1
}
mutating func add(_ newValue: Value) {
guard newValue != current else { return }
let numberOfValuesAfterCurrent = lastIndex - index
values.removeLast(numberOfValuesAfterCurrent)
values.append(newValue)
index += 1
}
}
This allows us to save any number of values, with the ability not only to get the current, previous, and next values but also to check whether a previous or a next value exists. There are two points to note about the add(_:)
method. First, it adds a new value to the history only if the new value is not equal to the value represented by the current
property. This is to avoid having consecutive duplicate values, which will cause the undo and redo feature to not work as expected. Second, it checks before adding a new value whether we have moved back some way through the history. If so, any values after the current position are discarded. This is also required for the undo and redo functionality to work as expected.
We no longer need to store a Calculator
value directly in the view model. Accordingly, we modify CalculatorViewModel
to replace the calculator
property with a calcualtorHistory
property. The value assigned to this property is an instance of ValueHistory
initialized with a new Calculator
value. We apply the @Published
property wrapper to the calculatorHistory
property. This will cause the view to be updated not only when a new Calculator
value is added to the history but also when we undo and redo actions.
@Published private var calculatorHistory = ValueHistory(Calculator())
We modify the displayText
property to use calculatorHistory
instead of calculator
.
var displayText: String? {
calculatorHistory.current.number.map(String.init)
}
We also modify the executeCommand(forIndex:)
method to pass the current value in calculatoryHistory
to the command to be executed, and to add the resulting new Calculator
value to calculatorHistory
.
func performAction(forButtonLabelIndex index: Int) {
let newValue = commands[index].execute(with: calculatorHistory.current)
calculatorHistory.add(newValue)
}
Finally, we add the following properties and methods to CalculatorViewModel
, which will be used by the view to present the undo and redo functionality to users.
var canUndo: Bool {
calculatorHistory.hasPrevious
}
var canRedo: Bool {
calculatorHistory.hasNext
func undo() {
calculatorHistory.previous()
}
func redo() {
calculatorHistory.next()
}
7. Enhancing the UI
Since we would want the view to disable the undo and redo buttons when these actions are not available, we add a disabled
property to the ButtonLabel
view modifier, which will allow us to change the foreground color of the button label when the button is disabled.
struct ButtonLabel: ViewModifier {
let disabled: Bool
func body(content: Content) -> some View {
content
.font(.title2)
.padding()
.frame(maxWidth: .infinity, alignment: .center)
.background(Color(.systemFill))
.foregroundColor(disabled ? Color(.tertiaryLabel) : Color(.label) )
.cornerRadius(5)
}
}
We also modify the the buttonLabel()
method in the View
extension we had defined earlier to include a disabled
parameter, with a default value of false
.
func buttonLabel(disabled: Bool = false) -> some View {
modifier(ButtonLabel(disabled: disabled))
}
All that is left to do now is to add undo and redo buttons to the body
property of ContentView
, as shown below.
var body: some View {
VStack {
Text(viewModel.displayText ?? " ")
.displayText()
LazyVGrid(columns: buttonColumns) {
ForEach(0..<viewModel.buttonLabels.count, id: \.self) { index in
Button(viewModel.buttonLabels[index]) {
viewModel.performAction(forButtonLabelIndex: index)
}
.buttonLabel()
}
}
HStack {
Button("Undo", action: viewModel.undo)
.buttonLabel(disabled: !viewModel.canUndo)
.disabled(!viewModel.canUndo)
Button("Redo", action: viewModel.redo)
.buttonLabel(disabled: !viewModel.canRedo)
.disabled(!viewModel.canRedo)
}
}
.padding()
}
Here is how our enhanced calculator looks in light mode when it first powers up, with the undo and redo buttons disabled. These buttons get enabled and disabled, as required, during the operation of the calculator.
This concludes our calculator example. The way we have designed the calculator, it is easy to customize and extend. Changing the key layout, for instance, is possible just by changing the JSON
file from which the button configuration is decoded. It is also relatively simple to add new functionality, such as adding new calculation actions, generating a calculation history, giving users a way to record, store and replay groups of calculation steps, etc.
Conclusion
Swift enables a new way of designing applications, with powerful value types used to model domain logic, processes and algorithms. We can also encapsulate actions in closures, which enables us to implement the Command Pattern without the complexity and boilerplate code of the object-oriented implementation. With domain models built using value types, it is also easier to undo and redo the effect of commands in a simpler and more efficient manner. With no need to worry up-front about identifying the right domain entities and abstractions, we can model domain logic by progressively composing value types rich in the functionality and language of the domain, making it easier to write simpler and more flexible, testable and concurrency-friendly code.
Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post.
Steven Berkowitz says
Really great article. Was searching about examples for command pattern in Swift, especially using SwiftUI. Exactly what I was looking for!
Khawer Khaliq says
Thanks Steven. Really glad that you found the article useful.