Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

19. Mediator 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 mediator pattern is a behavioral design pattern that encapsulates how objects communicate with one another. It involves four types:

  1. The colleagues are the objects that want to communicate with each other. They implement the colleague protocol.

  2. The colleague protocol defines methods and properties that each colleague must implement.

  3. The mediator is the object that controls the communication of the colleagues. It implements the mediator protocol.

  4. The mediator protocol defines methods and properties that the mediator must implement.

Each colleague contains a reference to the mediator, via the mediator protocol. In lieu of interacting with other colleagues directly, each colleague communicates through the mediator.

The mediator facilitates colleague-to-colleague interaction: Colleagues may both send and receive messages from the mediator.

When should you use it?

This mediator pattern is useful to separate interactions between colleagues into an object, the mediator.

This pattern is especially useful when you need one or more colleagues to act upon events initiated by another colleague, and, in turn, have this colleague generate further events that affect other colleagues.

Playground example

Open AdvancedDesignPattern.xcworkspace in the Starter directory, or continue from your own playground workspace you’ve been continuing to work on throughout the book, and then open the Mediator page from the File hierarchy.

// 1
open class Mediator<ColleagueType> {

  // 2
  private class ColleagueWrapper {
    var strongColleague: AnyObject?
    weak var weakColleague: AnyObject?

    // 3
    var colleague: ColleagueType? {
      return 
        (weakColleague ?? strongColleague) as? ColleagueType
    }

    // 4
    init(weakColleague: ColleagueType) {
      self.strongColleague = nil
      self.weakColleague = weakColleague as AnyObject
    }

    init(strongColleague: ColleagueType) {
      self.strongColleague = strongColleague  as AnyObject
      self.weakColleague = nil
    }
  }
}
// MARK: - Instance Properties
// 1
private var colleagueWrappers: [ColleagueWrapper] = []

// 2
public var colleagues: [ColleagueType] {
  var colleagues: [ColleagueType] = []
  colleagueWrappers = colleagueWrappers.filter {
    guard let colleague = $0.colleague else { return false }
    colleagues.append(colleague)
    return true
  }
  return colleagues
}

// MARK: - Object Lifecycle
// 3
public init() { }
// MARK: - Colleague Management
// 1
public func addColleague(_ colleague: ColleagueType,
                         strongReference: Bool = true) {
  let wrapper: ColleagueWrapper
  if strongReference {
    wrapper = ColleagueWrapper(strongColleague: colleague)
  } else {
    wrapper = ColleagueWrapper(weakColleague: colleague)
  }
  colleagueWrappers.append(wrapper)
}

// 2
public func removeColleague(_ colleague: ColleagueType) {
  guard let index = colleagues.firstIndex(where: {
    ($0 as AnyObject) === (colleague as AnyObject)
  }) else { return }
  colleagueWrappers.remove(at: index)
}
public func invokeColleagues(closure: (ColleagueType) -> Void) {
  colleagues.forEach(closure)
}

public func invokeColleagues(by colleague: ColleagueType,
                             closure: (ColleagueType) -> Void) {
  colleagues.forEach {
    guard ($0 as AnyObject) !== (colleague as AnyObject)
      else { return }
    closure($0)
  }
}
// MARK: - Colleague Protocol
public protocol Colleague: class {
  func colleague(_ colleague: Colleague?,
                 didSendMessage message: String)
}
// MARK: - Mediator Protocol
public protocol MediatorProtocol: class {
  func addColleague(_ colleague: Colleague)
  func sendMessage(_ message: String, by colleague: Colleague)
}
// MARK: - Colleague
// 1
public class Musketeer {

  // 2
  public var name: String
  public weak var mediator: MediatorProtocol?

  // 3
  public init(mediator: MediatorProtocol, name: String) {
    self.mediator = mediator
    self.name = name
    mediator.addColleague(self)
  }

  // 4
  public func sendMessage(_ message: String) {
    print("\(name) sent: \(message)")
    mediator?.sendMessage(message, by: self)
  }
}
extension Musketeer: Colleague {
  public func colleague(_ colleague: Colleague?,
                        didSendMessage message: String) {
    print("\(name) received: \(message)")
  }
}
// MARK: - Mediator
// 1
public class MusketeerMediator: Mediator<Colleague> {

}
extension MusketeerMediator: MediatorProtocol {

  // 2
  public func addColleague(_ colleague: Colleague) {
    self.addColleague(colleague, strongReference: true)
  }

  // 3
  public func sendMessage(_ message: String,
                          by colleague: Colleague) {
    invokeColleagues(by: colleague) {
      $0.colleague(colleague, didSendMessage: message)
    }
  }
}
// MARK: - Example
let mediator = MusketeerMediator()
let athos = Musketeer(mediator: mediator, name: "Athos")
let porthos = Musketeer(mediator: mediator, name: "Porthos")
let aramis = Musketeer(mediator: mediator, name: "Aramis")
athos.sendMessage("One for all...!")
print("")

porthos.sendMessage("and all for one...!")
print("")

aramis.sendMessage("Unus pro omnibus, omnes pro uno!")
print("")
Athos sent: One for all...!
Porthos received: One for all...!
Aramis received: One for all...!

Porthos sent: and all for one...!
Athos received: and all for one...!
Aramis received: and all for one...!

Aramis sent: Unus pro omnibus, omnes pro uno!
Athos received: Unus pro omnibus, omnes pro uno!
Porthos received: Unus pro omnibus, omnes pro uno!
mediator.invokeColleagues() {
  $0.colleague(nil, didSendMessage: "Charge!")
}
Athos received: Charge!
Porthos received: Charge!
Aramis received: Charge!

What should you be careful about?

This pattern is very useful in decoupling colleagues. Instead of colleagues interacting directly, each colleague communicates through the mediator.

Tutorial project

In this chapter, you’ll add functionality to an app called YetiDate. This app will help users plan a date that involves three different locations: a bar, restaurant and movie theater. It uses CocoaPods to pull in YelpAPI, a helper library for searching Yelp for said venues.

Registering for a Yelp API key

If you worked through CoffeeQuest in the Intermediate Section, you’ve already created a Yelp API key. You would have done this in Chapter 10, “Model-View-ViewModel Pattern”. Copy your existing key and paste it where indicated within APIKeys.swift, then skip the rest of this section and head to the “Creating required protocols” section.

Creating required protocols

Since the app shows nearby restaurants, bars and movie theaters, it works best for areas with many businesses nearby. So the app’s default location has been set to San Francisco, California.

searchClient.update(userCoordinate: userLocation.coordinate)

import CoreLocation.CLLocation
import YelpAPI

// 1
public protocol SearchColleague: class {

  // 2
  var category: YelpCategory { get }
  var selectedBusiness: YLPBusiness? { get }

  // 3
  func update(userCoordinate: CLLocationCoordinate2D)

  // 4
  func fellowColleague(_ colleague: SearchColleague,
                       didSelect business: YLPBusiness)

  // 5
  func reset()
}
import YelpAPI

public protocol SearchColleagueMediating: class {

  // 1
  func searchColleague(
    _ searchColleague: SearchColleague,
    didSelect business: YLPBusiness)

  // 2
  func searchColleague(
    _ searchColleague: SearchColleague,
    didCreate viewModels: Set<BusinessMapViewModel>)

  // 3
  func searchColleague(
    _ searchColleague: SearchColleague,
    searchFailed error: Error?)
}
import CoreLocation
import YelpAPI

public class YelpSearchColleague {

  // 1
  public let category: YelpCategory
  public private(set) var selectedBusiness: YLPBusiness?

  // 2
  private var colleagueCoordinate: CLLocationCoordinate2D?
  private unowned let mediator: SearchColleagueMediating
  private var userCoordinate: CLLocationCoordinate2D?
  private let yelpClient: YLPClient

  // 3
  private static let defaultQueryLimit = UInt(20)
  private static let defaultQuerySort = YLPSortType.bestMatched
  private var queryLimit = defaultQueryLimit
  private var querySort = defaultQuerySort

  // 4
  public init(category: YelpCategory,
              mediator: SearchColleagueMediating) {
    self.category = category
    self.mediator = mediator
    self.yelpClient = YLPClient(apiKey: YelpAPIKey)
  }
}
// MARK: - SearchColleague
// 1
extension YelpSearchColleague: SearchColleague {

  // 2
  public func fellowColleague(_ colleague: SearchColleague,
                              didSelect business: YLPBusiness) {
    colleagueCoordinate = CLLocationCoordinate2D(
      business.location.coordinate)
    queryLimit /= 2
    querySort = .distance
    performSearch()
  }

  // 3
  public func update(userCoordinate: CLLocationCoordinate2D) {
    self.userCoordinate = userCoordinate
    performSearch()
  }

  // 4
  public func reset() {
    colleagueCoordinate = nil
    queryLimit = YelpSearchColleague.defaultQueryLimit
    querySort = YelpSearchColleague.defaultQuerySort
    selectedBusiness = nil
    performSearch()
  }

  private func performSearch() {
    // TODO
  }
}
// 1
guard selectedBusiness == nil,
  let coordinate = colleagueCoordinate ??
    userCoordinate else { return }

// 2
let yelpCoordinate = YLPCoordinate(
  latitude: coordinate.latitude,
  longitude: coordinate.longitude)
let query = YLPQuery(coordinate: yelpCoordinate)
query.categoryFilter = [category.rawValue]
query.limit = queryLimit
query.sort = querySort

yelpClient.search(with: query) {
  [weak self] (search, error) in
  guard let self = self else { return }
  guard let search = search else {
    // 3
    self.mediator.searchColleague(self,
                                  searchFailed: error)
    return
  }
  // 4
  var set: Set<BusinessMapViewModel> = []
  for business in search.businesses {
    guard let coordinate = business.location.coordinate
      else { continue }
    let viewModel = BusinessMapViewModel(
      business: business,
      coordinate: coordinate,
      primaryCategory: self.category,
      onSelect: { [weak self] business in
        guard let self = self else { return }
        self.selectedBusiness = business
        self.mediator.searchColleague(self,
                                      didSelect: business)
    })
    set.insert(viewModel)
  }

  // 5
  DispatchQueue.main.async {
    self.mediator.searchColleague(self, didCreate: set)
  }
}
public class SearchClient: Mediator<SearchColleague> {
// MARK: - SearchColleagueMediating
// 1
extension SearchClient: SearchColleagueMediating {

  // 2
  public func searchColleague(
    _ searchColleague: SearchColleague,
    didSelect business: YLPBusiness) {

    delegate?.searchClient(self,
                           didSelect: business,
                           for: searchColleague.category)

    invokeColleagues(by: searchColleague) { colleague in
      colleague.fellowColleague(colleague, didSelect: business)
    }

    notifyDelegateIfAllBusinessesSelected()
  }

  private func notifyDelegateIfAllBusinessesSelected() {
    guard let delegate = delegate else { return }
    var categoryToBusiness: [YelpCategory : YLPBusiness] = [:]
    for colleague in colleagues {
      guard let business = colleague.selectedBusiness else {
        return
      }
      categoryToBusiness[colleague.category] = business
    }
    delegate.searchClient(
      self,
      didCompleteSelection: categoryToBusiness)
  }

  // 3
  public func searchColleague(
    _ searchColleague: SearchColleague,
    didCreate viewModels: Set<BusinessMapViewModel>) {

    delegate?.searchClient(self,
                           didCreate: viewModels,
                           for: searchColleague.category)
  }

  // 4
  public func searchColleague(
    _ searchColleague: SearchColleague,
    searchFailed error: Error?) {
    
    delegate?.searchClient(self,
                           failedFor: searchColleague.category,
                           error: error)
  }
}
let restaurantColleague = YelpSearchColleague(
  category: .restaurants, mediator: self)
addColleague(restaurantColleague)

let barColleague = YelpSearchColleague(
  category: .bars, mediator: self)
addColleague(barColleague)

let movieColleague = YelpSearchColleague(
  category: .movieTheaters, mediator: self)
addColleague(movieColleague)
invokeColleagues() { colleague in
  colleague.update(userCoordinate: userCoordinate)
}
invokeColleagues() { colleague in
  colleague.reset()
}

Key points

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

Where to go from here?

You also created Yeti Dates in this chapter! This is a neat app, but there’s a lot more you can do with it:

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