Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

21. Command Pattern
Written by Joshua Greene

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

The command pattern is a behavioral pattern that encapsulates information to perform an action into a command object. It involves three types:

  1. The invoker stores and executes commands.
  2. The command encapsulates the action as an object.
  3. The receiver is the object that’s acted upon by the command.

Hence, this pattern allows you to model the concept of executing an action.

When should you use it?

Use this pattern whenever you want to create actions that can be executed on receivers at a later point in time. For example, you can create and store commands to be performed by a computer AI, and then execute these over time.

Playground example

Open AdvancedDesignPatterns.xcworkspace in the Starter directory, and then open the Command page.

import Foundation

// MARK: - Receiver
public class Door {
  public var isOpen = false
}
// MARK: - Command
// 1
public class DoorCommand {
  public let door: Door
  public init(_ door: Door) {
    self.door = door
  }
  public func execute() { }
}

// 2
public class OpenCommand: DoorCommand {
  public override func execute() {
    print("opening the door...")
    door.isOpen = true
  }
}

// 3
public class CloseCommand: DoorCommand {
  public override func execute() {
    print("closing the door...")
    door.isOpen = false
  }
}
// MARK: - Invoker
// 1
public class Doorman {

  // 2
  public let commands: [DoorCommand]
  public let door: Door

  // 3
  public init(door: Door) {
    let commandCount = arc4random_uniform(10) + 1
    self.commands = (0 ..< commandCount).map { index in
      return index % 2 == 0 ?
        OpenCommand(door) : CloseCommand(door)
    }
    self.door = door
  }

  // 4
  public func execute() {
    print("Doorman is...")
    commands.forEach { $0.execute() }
  }
}
// MARK: - Example
public let isOpen = true
print("You predict the door will be " +
  "\(isOpen ? "open" : "closed").")
print("")
You predict the door will be open.
let door = Door()
let doorman = Doorman(door: door)
doorman.execute()
print("")
Doorman is...
opening the door...
closing the door...
opening the door...
if door.isOpen == isOpen {
  print("You were right! :]")
} else {
  print("You were wrong :[")
}
print("The door is \(door.isOpen ? "open" : "closed").")
You were right!
The door is open.

What should you be careful about?

The command pattern can result in many command objects. Consequently, this can lead to code that’s harder to read and maintain. If you don’t need to perform actions later, you may be better off simply calling the receiver’s methods directly.

Tutorial project

You’ll build a game app called RayWenToe in this chapter. This is a variation on TicTacToe. Here are the rules:

Building your game

Open Finder and navigate to where you downloaded the resources for this chapter. Then, open starter\RayWenToe\RayWenToe.xcodeproj in Xcode.

Creating and storing command objects

Add a new Swift file called MoveCommand.swift to the GameManager group, which is a subgroup within the Controllers group, and replace its contents with the following:

// 1
public struct MoveCommand {

  // 2
  public var gameboard: Gameboard

  // 3
  public var gameboardView: GameboardView

  // 4
  public var player: Player

  // 5
  public var position: GameboardPosition
}
public func execute(completion: (() -> Void)? = nil) {
  // 1
  gameboard.setPlayer(player, at: position)

  // 2
  gameboardView.placeMarkView(
    player.markViewPrototype.copy(), at: position,
    animated: true, completion: completion)
}
internal lazy var movesForPlayer =
  [player1: [MoveCommand](), player2: [MoveCommand]()]
public var movesForPlayer: [Player: [MoveCommand]] {
  get { return gameManager.movesForPlayer }
  set { gameManager.movesForPlayer = newValue }
}
// 1
public override func addMove(at position: GameboardPosition) {

  // 2
  let moveCount = movesForPlayer[player]!.count
  guard moveCount < turnsPerPlayer else { return }

  // 3
  displayMarkView(at: position, turnNumber: moveCount + 1)

  // 4
  enqueueMoveCommand(at: position)
  updateMoveCountLabel()
}

Implementing move commands

Replace the contents of enqueueMoveCommand(at:) with the following:

let newMove = MoveCommand(gameboard: gameboard,
                          gameboardView: gameboardView,
                          player: player,
                          position: position)

movesForPlayer[player]!.append(newMove)
let turnsRemaining = turnsPerPlayer - movesForPlayer[player]!.count
gameplayView.moveCountLabel.text =
  "\(turnsRemaining) Moves Left"

guard movesForPlayer[player]!.count == turnsPerPlayer
  else { return }
gameManager.transitionToNextState()
// 1
var moves = movesForPlayer[player]!
guard let position = moves.popLast()?.position else { return }

// 2
movesForPlayer[player] = moves
updateMoveCountLabel()

// 3
let markView = gameboardView.markViewForPosition[position]!
_ = markView.turnNumbers.popLast()

// 4
guard markView.turnNumbers.count == 0 else { return }
gameboardView.removeMarkView(at: position, animated: false)
private func combinePlayerMoves() -> [MoveCommand] {
  var result: [MoveCommand] = []
  let player1Moves = movesForPlayer[player1]!
  let player2Moves = movesForPlayer[player2]!
  assert(player1Moves.count == player2Moves.count)
  for i in 0 ..< player1Moves.count {
    result.append(player1Moves[i])
    result.append(player2Moves[i])
  }
  return result
}
private func performMove(at index: Int,
                         with moves: [MoveCommand]) {

  // 1
  guard index < moves.count else {
    displayWinner()
    return
  }

  // 2
  let move = moves[index]
  move.execute(completion: { [weak self] in
    self?.performMove(at: index + 1, with: moves)
  })
}
let gameMoves = combinePlayerMoves()
performMove(at: 0, with: gameMoves)

movesForPlayer = [player1: [], player2: []]
movesForPlayer[player] = positions.map {
  MoveCommand(gameboard: gameboard,
              gameboardView: gameboardView,
              player: player,
              position: $0)
}
gameManager.transitionToNextState()

Key points

You learned about the command pattern in this chapter. Here are its key points:

Where to go from here?

You created a fun variant of TicTacToe where players select their moves in advance. There’s still a lot of functionality and changes you can make to RayWenToe:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now