Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

5. Strategy Pattern
Written by Joshua Greene

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.

You’ll see that Strategy is listed under Behavioral Patterns. This is because the strategy pattern is about one object using another to do something.

Click on the Strategy link to open that page.

For the code example, consider an app that uses several “movie rating services” such as Rotten Tomatoes®, IMDb and Metacritic. Instead of writing code for each of these services directly within a view controller, and likely having complex if-else statements therein, you can use the strategy pattern to simplify things by creating a protocol that defines a common API for every service.

First, you need to create a strategy protocol. Add the following right after Code example:

import UIKit

public protocol MovieRatingStrategy {
  // 1
  var ratingServiceName: String { get }
  
  // 2
  func fetchRating(for movieTitle: String,
    success: (_ rating: String, _ review: String) -> ())
}
  1. You’ll use ratingServiceName to display which service provided the rating. For example, this would return “Rotten Tomatoes.”

  2. You’ll use fetchRatingForMovieTitle(_:success:) to fetch movie ratings asynchronously. In a real app, you’d also likely have a failure closure too, as networking calls don’t always succeed.

Next, add the following implementation for RottenTomatoesClient:

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)
  }
}

Finally, add the following implementation for IMDbClient:

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)
  }
}

Since both of these clients conform to MovieRatingStrategy, consuming objects don’t need to know about either directly. Instead, they can depend on the protocol alone.

For example, add the following code at the end of the file:

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
    }
  }
}

Whenever this view controller is instantiated within the app (however that happens), you’d need to set the movieRatingClient. Notice how the view controller doesn’t know about the concrete implementations of MovieRatingStrategy.

The determination of which MovieRatingStrategy to use can be deferred until runtime, and this could even be selected by the user if your app allowed that.

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.

Instead of always showing the questions in the same order each time, wouldn’t it be great if they were randomized? However, some users may also want to study the questions in order. You’ll use the strategy pattern to allow both options!

Right-click on the yellow RabbleWabble group, select New Group and name it Strategies.

Right-click again on the yellow RabbleWabble group and select Sort by Name.

Your File hierarchy should now look like this:

Right-click on your newly-added Strategies group and select New File. Under the iOS tab, select Swift File and press Next. Enter QuestionStrategy.swift for the name and press Create.

Replace the contents of QuestionStrategy.swift with the following:

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
}

This creates the protocol at the heart of the strategy pattern you’re going to use.

Here’s how you’ll use each of the parts of the protocol:

  1. title will be the title for which set of questions is selected, such as "Basic Phrases."

  2. correctCount and incorrectCount will return the current number of correct and incorrect questions, respectively.

  3. advanceToNextQuestion() will be used to move onto the next question. If there isn’t a next question available, this method will return false. Otherwise, it will return true.

  4. currentQuestion() will simply return the current question. Since advanceToNextQuestion() will prevent the user from advancing beyond the available questions, currentQuestion() will always return a Question and never be nil.

  5. As their method names imply, markQuestionCorrect(_:) will mark a question correct, and markQuestionIncorrect(_:) will mark a question incorrect.

  6. questionIndexTitle() will return the “index title” for the current question to indicate progress, such as "1 / 10" for the first question out of ten total.

Create another file under the Strategies group called SequentialQuestionStrategy.swift. Replace its contents with the following:

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)"
  }
}

SequentialQuestionStrategy takes a QuestionGroup via its designated initializer, init(questionGroup:), and it essentially functions just like the app currently does; it goes from one question to the next in the order defined by questionGroup.questions.

Create another file under the Strategies group called RandomQuestionStrategy.swift. Replace its contents with the following:

// 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)"
  }
}

Let’s go over the interesting parts:

  1. While you could implement randomization logic yourself, GameplayKit.GKRandomSource already does it for you, and it works really well. Despite the GameplayKit name, this is actually a fairly small and scoped import here, so there’s really not a downside to using it.

  2. Here you use the GKRandomSource.sharedRandom(), which is the “default” or singleton instance of GKRandomSource. Another design pattern! Apple frameworks are full of them, and you’ll learn about this pattern in the next chapter. For now, simply accept that it gives you an instance of GKRandomSource.

    The method arrayByShufflingObjects does exactly as it says: It takes an array and randomly shuffles the elements. It’s just what you need here! The only downside is that it returns an NSArray, as Apple is still adopting Swift fully throughout its core frameworks. However, you can simply cast this to [Question], and you’ll be good to go!

Next, you need to update QuestionViewController to use a QuestionStrategy instead of using a QuestionGroup directly.

Open QuestionViewController.swift and add the following property right below delegate:

public var questionStrategy: QuestionStrategy! {
  didSet {
    navigationItem.title = questionStrategy.title
  }
}

Next, replace showQuestion() with the following:

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()
}

Here you use the questionStrategy to get the (1) currentQuestion() and (2) questionIndexTitle() instead of getting these from the questionGroup.

Finally, replace handleCorrect(_:) and handleIncorrect(_:) with the following:

@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()
}

You again replace uses of questionGroup with questionStrategy instead.

The last method you need to update is showNextQuestion(). However, this is a bit trickier because you call the delegate method, and this takes in a questionGroup parameter.

You’re faced with a choice now: You can either add questionGroup to the QuestionStrategy protocol, or update the QuestionViewControllerDelegate method to use QuestionStrategy instead of QuestionGroup.

When faced with a choice like this in your own apps, you should try to consider the consequences of each:

  • If you expose the QuestionGroup, will this make the overall app design messier and harder to maintain?
  • If you change this to QuestionStrategy instead of QuestionGroup, will you later actually need the QuestionGroup?
  • Do existing classes that implement QuestionViewControllerDelegate use and rely on the QuestionGroup parameter?

Depending on your answers, you’d need to choose one or the other… fortunately, you have a 50/50 shot of being right (or wrong)!

In this case, another developer (ahem, one who knows what’s coming up in the next few chapters), advises that you update the QuestionViewControllerDelegate and change the QuestionGroup to QuestionStrategy instead.

Replace the existing QuestionViewControllerDelegate protocol with the following (ignore the compiler errors for now):

public protocol QuestionViewControllerDelegate: class {
  func questionViewController(
    _ viewController: QuestionViewController,
    didCancel questionGroup: QuestionStrategy)
  
  func questionViewController(
    _ viewController: QuestionViewController,
    didComplete questionStrategy: QuestionStrategy)
}

Next, scroll down and replace showNextQuestion() with the following:

private func showNextQuestion() {
  guard questionStrategy.advanceToNextQuestion() else {
    delegate?.questionViewController(self,
      didComplete: questionStrategy)
    return
  }
  showQuestion()
}

Finally, you also need to replace handleCancelPressed(sender:) with the following:

@objc private func handleCancelPressed(
  sender: UIBarButtonItem) {
  
  delegate?.questionViewController(self,
    didCancel: questionStrategy)
}

Since you’ve updated all places that use questionGroup directly, delete the questionGroup property.

At this point, you shouldn’t see any compiler errors or warnings on the QuestionViewController. However, if you try to build and run, you’ll still get compiler errors.

This is because you also need to update the SelectQuestionViewController, which creates QuestionViewController instances and implements QuestionViewControllerDelegate.

Open SelectQuestionGroupViewController.swift and replace this line in prepare(for:sender:):

viewController.questionGroup = selectedQuestionGroup

…with the following:

viewController.questionStrategy = RandomQuestionStrategy(questionGroup: selectedQuestionGroup)

Finally, replace the entire extension that implements QuestionViewControllerDelegate with the following:

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)
  }
}

Build and run your project. Select any cell, press the green check or red X buttons a few times, press Back, then press the same cell again and repeating the process. You should see that the questions are now randomized!

Switch back to SelectQuestionGroupViewController.swift in Xcode, and replace this line within prepare(for:sender:):

viewController.questionStrategy = RandomQuestionStrategy(questionGroup: selectedQuestionGroup)

…with this instead:

viewController.questionStrategy = SequentialQuestionStrategy(questionGroup: selectedQuestionGroup)

Build and run and try working through the same set of questions again. This time, they should now be in the same order.

How cool is that? You can now easily swap out different strategies as necessary!

Key points

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

  • The strategy pattern defines a family of interchangeable objects that can be set or switched at runtime.

  • This pattern has three parts: an object using a strategy, a strategy protocol, and a family of strategy objects.

  • The strategy pattern is similar to the delegation pattern: Both patterns use a protocol for flexibility. Unlike the delegation pattern, however, strategies are meant to be switched at runtime, whereas delegates are usually fixed.

You’ve laid the groundwork for Rabble Wabble to switch question strategies at runtime. However, you haven’t actually created a means for the user to do this while running the app just yet! There’s another pattern you’ll use to hold onto user preferences like this: the singleton design pattern.

Continue onto the next chapter to learn about the singleton design pattern and continue building out Rabble Wabble.

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.
© 2026 Kodeco Inc.