Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

5. Strategy 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 strategy pattern defines a family of interchangeable objects that can be set or switched at runtime. This pattern has three parts:

  • The object using a strategy. This is most often a view controller when the pattern is used in iOS app development, but it can technically be any kind of object that needs interchangeable behavior.

  • The strategy protocol defines methods that every strategy must implement.

  • The strategies are objects that conform to the strategy protocol.

When should you use it?

Use the strategy pattern when you have two or more different behaviors that are interchangeable.

This pattern is similar to the delegation pattern: both patterns rely on a protocol instead of concrete objects for increased flexibility. Consequently, any object that implements the strategy protocol can be used as a strategy at runtime.

Unlike delegation, the strategy pattern uses a family of objects.

Delegates are often fixed at runtime. For example, the dataSource and delegate for a UITableView can be set from Interface Builder, and it’s rare for these to change during runtime.

Strategies, however, are intended to be easily interchangeable at runtime.

Playground example

Open FundamentalDesignPatterns.xcworkspace in the Starter directory and then open the Overview page.

import UIKit

public protocol MovieRatingStrategy {
  // 1
  var ratingServiceName: String { get }
  
  // 2
  func fetchRating(for movieTitle: String,
    success: (_ rating: String, _ review: String) -> ())
}
public class RottenTomatoesClient: MovieRatingStrategy {
  public let ratingServiceName = "Rotten Tomatoes"
  
  public func fetchRating(
    for movieTitle: String,
    success: (_ rating: String, _ review: String) -> ()) {
    
    // In a real service, you’d make a network request...
    // Here, we just provide dummy values...
    let rating = "95%"
    let review = "It rocked!"
    success(rating, review)
  }
}
public class IMDbClient: MovieRatingStrategy {
  public let ratingServiceName = "IMDb"
  
  public func fetchRating(
    for movieTitle: String,
    success: (_ rating: String, _ review: String) -> ()) {
    
    let rating = "3 / 10"
    let review = """
      It was terrible! The audience was throwing rotten
      tomatoes!
      """
    success(rating, review)
  }
}
public class MovieRatingViewController: UIViewController {
  
  // MARK: - Properties
  public var movieRatingClient: MovieRatingStrategy!
  
  // MARK: - Outlets
  @IBOutlet public var movieTitleTextField: UITextField!
  @IBOutlet public var ratingServiceNameLabel: UILabel!
  @IBOutlet public var ratingLabel: UILabel!
  @IBOutlet public var reviewLabel: UILabel!
  
  // MARK: - View Lifecycle
  public override func viewDidLoad() {
    super.viewDidLoad()
    ratingServiceNameLabel.text = 
      movieRatingClient.ratingServiceName
  }
  
  // MARK: - Actions
  @IBAction public func searchButtonPressed(sender: Any) {
    guard let movieTitle = movieTitleTextField.text
      else { return }
    
    movieRatingClient.fetchRating(for: movieTitle) {
      (rating, review) in
      self.ratingLabel.text = rating
      self.reviewLabel.text = review
    }
  }
}

What should you be careful about?

Be careful about overusing this pattern. In particular, if a behavior won’t ever change, it’s okay to put this directly within the consuming view controller or object context. The trick to this pattern is knowing when to pull out behaviors, and it’s okay to do this lazily as you determine where it’s needed.

Tutorial project

You’ll continue the RabbleWabble app from the previous chapter. If you skipped the previous chapter, or you want a fresh start, open Finder and navigate to where you downloaded the resources for this chapter, and then open starter ▸ RabbleWabble ▸ RabbleWabble.xcodeproj in Xcode.

public protocol QuestionStrategy: class {
  // 1  
  var title: String { get }
  
  // 2
  var correctCount: Int { get }
  var incorrectCount: Int { get }
  
  // 3
  func advanceToNextQuestion() -> Bool
  
  // 4
  func currentQuestion() -> Question
  
  // 5
  func markQuestionCorrect(_ question: Question)
  func markQuestionIncorrect(_ question: Question)
  
  // 6
  func questionIndexTitle() -> String
}
public class SequentialQuestionStrategy: QuestionStrategy {
  // MARK: - Properties
  public var correctCount: Int = 0
  public var incorrectCount: Int = 0
  private let questionGroup: QuestionGroup
  private var questionIndex = 0
  
  // MARK: - Object Lifecycle
  public init(questionGroup: QuestionGroup) {
    self.questionGroup = questionGroup
  }

  // MARK: - QuestionStrategy
  public var title: String {
    return questionGroup.title
  }
  
  public func currentQuestion() -> Question {
    return questionGroup.questions[questionIndex]
  }
  
  public func advanceToNextQuestion() -> Bool {
    guard questionIndex + 1 < 
      questionGroup.questions.count else {
      return false
    }
    questionIndex += 1
    return true
  }
  
  public func markQuestionCorrect(_ question: Question) {
    correctCount += 1
  }
  
  public func markQuestionIncorrect(_ question: Question) {
    incorrectCount += 1
  }
  
  public func questionIndexTitle() -> String {
    return "\(questionIndex + 1)/" + 
      "\(questionGroup.questions.count)"
  }
}
// 1
import GameplayKit.GKRandomSource

public class RandomQuestionStrategy: QuestionStrategy {
  // MARK: - Properties
  public var correctCount: Int = 0
  public var incorrectCount: Int = 0
  private let questionGroup: QuestionGroup
  private var questionIndex = 0
  private let questions: [Question]
  
  // MARK: - Object Lifecycle
  public init(questionGroup: QuestionGroup) {
    self.questionGroup = questionGroup
    
    // 2
    let randomSource = GKRandomSource.sharedRandom()
    self.questions = 
      randomSource.arrayByShufflingObjects(
      in: questionGroup.questions) as! [Question]
  }
  
  // MARK: - QuestionStrategy
  public var title: String {
    return questionGroup.title
  }
  
  public func currentQuestion() -> Question {
    return questions[questionIndex]
  }
  
  public func advanceToNextQuestion() -> Bool {
    guard questionIndex + 1 < questions.count else {
      return false
    }
    questionIndex += 1
    return true
  }
  
  public func markQuestionCorrect(_ question: Question) {
    correctCount += 1
  }
  
  public func markQuestionIncorrect(_ question: Question) {
    incorrectCount += 1
  }
  
  public func questionIndexTitle() -> String {
    return "\(questionIndex + 1)/\(questions.count)"
  }
}
public var questionStrategy: QuestionStrategy! {
  didSet {
    navigationItem.title = questionStrategy.title
  }
}
private func showQuestion() {
  // 1
  let question = questionStrategy.currentQuestion()
  
  questionView.answerLabel.text = question.answer
  questionView.promptLabel.text = question.prompt
  questionView.hintLabel.text = question.hint
  
  questionView.answerLabel.isHidden = true
  questionView.hintLabel.isHidden = true
  
  // 2
  questionIndexItem.title = 
    questionStrategy.questionIndexTitle()
}
@IBAction func handleCorrect(_ sender: Any) {
  let question = questionStrategy.currentQuestion()
  questionStrategy.markQuestionCorrect(question)
  
  questionView.correctCountLabel.text =
    String(questionStrategy.correctCount)
  showNextQuestion()
}

@IBAction func handleIncorrect(_ sender: Any) {
  let question = questionStrategy.currentQuestion()
  questionStrategy.markQuestionIncorrect(question)
  
  questionView.incorrectCountLabel.text =
    String(questionStrategy.incorrectCount)
  showNextQuestion()
}

public protocol QuestionViewControllerDelegate: class {
  func questionViewController(
    _ viewController: QuestionViewController,
    didCancel questionGroup: QuestionStrategy)
  
  func questionViewController(
    _ viewController: QuestionViewController,
    didComplete questionStrategy: QuestionStrategy)
}
private func showNextQuestion() {
  guard questionStrategy.advanceToNextQuestion() else {
    delegate?.questionViewController(self,
      didComplete: questionStrategy)
    return
  }
  showQuestion()
}
@objc private func handleCancelPressed(
  sender: UIBarButtonItem) {
  
  delegate?.questionViewController(self,
    didCancel: questionStrategy)
}
viewController.questionGroup = selectedQuestionGroup
viewController.questionStrategy = RandomQuestionStrategy(questionGroup: selectedQuestionGroup)
extension SelectQuestionGroupViewController: QuestionViewControllerDelegate {
  
  public func questionViewController(
    _ viewController: QuestionViewController,
    didCancel questionGroup: QuestionStrategy) {
    navigationController?.popToViewController(self,
      animated: true)
  }
  
  public func questionViewController(
    _ viewController: QuestionViewController,
    didComplete questionGroup: QuestionStrategy) {
    navigationController?.popToViewController(self,
      animated: true)
  }
}
viewController.questionStrategy = RandomQuestionStrategy(questionGroup: selectedQuestionGroup)
viewController.questionStrategy = SequentialQuestionStrategy(questionGroup: selectedQuestionGroup)

Key points

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

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