Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

12. Adapter Pattern
Written by Jay Strawn

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

The adapter pattern is a behavioral pattern that allows incompatible types to work together. It involves four components:

  1. An object using an adapter is the object that depends on the new protocol.

  2. The new protocol is the desired protocol for use.

  3. A legacy object existed before the protocol was made and cannot be modified directly to conform to it.

  4. An adapter is created to conform to the protocol and passes calls onto the legacy object.

A great example of a physical adapter comes to mind when you consider the latest iPhone — there’s no headphone jack! If you want to plug your 3.5mm headphones into the lightning port, you need an adapter with a lightning connector on one end and a 3.5mm jack on the other.

This is essentially what the Adapter Pattern is about: connecting two elements that otherwise won’t “fit” with each other.

When should you use it?

Classes, modules, and functions can’t always be modified, especially if they’re from a third-party library. Sometimes you have to adapt instead!

You can create an adapter either by extending an existing class, or creating a new adapter class. This chapter will show you how to do both.

Playground example

Open IntermediateDesignPattern.xcworkspace in the Starter directory, or continue from your own playground workspace from the last chapter, then open the Adapter page.

import UIKit

// MARK: - Legacy Object
public  class GoogleAuthenticator {
  public func login(
    email: String,
    password: String,
    completion: @escaping (GoogleUser?, Error?) -> Void) {
    
    // Make networking calls that return a token string
    let token = "special-token-value"
    
    let user = GoogleUser(email: email,
                          password: password,
                          token: token)
    completion(user, nil)
  }
}

public struct GoogleUser {
  public var email: String
  public var password: String
  public var token: String
}
// MARK: - New Protocol
public protocol AuthenticationService {
  func login(email: String,
             password: String,
             success: @escaping (User, Token) -> Void,
             failure: @escaping (Error?) -> Void)
}

public struct User {
  public let email: String
  public let password: String
}

public struct Token {
  public let value: String
}
// MARK: - Adapter
// 1
public class GoogleAuthenticatorAdapter: AuthenticationService {
  
  // 2
  private var authenticator = GoogleAuthenticator()
  
  // 3
  public func login(email: String,
                    password: String,
                    success: @escaping (User, Token) -> Void,
                    failure: @escaping (Error?) -> Void) {
    
    authenticator.login(email: email, password: password) { 
      (googleUser, error) in

      // 4
      guard let googleUser = googleUser else {
        failure(error)
        return
      }
      
      // 5
      let user = User(email: googleUser.email,
                      password: googleUser.password)

      let token = Token(value: googleUser.token)
      success(user, token)
    }
  }
}
// MARK: - Object Using an Adapter
// 1
public class LoginViewController: UIViewController {
  
  // MARK: - Properties
  public var authService: AuthenticationService!
  
  // MARK: - Views
  var emailTextField = UITextField()
  var passwordTextField = UITextField()
  
  // MARK: - Class Constructors
  // 2
  public class func instance(
    with authService: AuthenticationService)
      -> LoginViewController {
      let viewController = LoginViewController()
      viewController.authService = authService
      return viewController
  }
  
  // 3
  public func login() {
    guard let email = emailTextField.text,
      let password = passwordTextField.text else {
        print("Email and password are required inputs!")
        return
    }
    authService.login(
      email: email,
      password: password,
      success: { user, token in
        print("Auth succeeded: \(user.email), \(token.value)")
    },
      failure: { error in
        print("Auth failed with error: no error provided")
    })
  }
}
// MARK: - Example
let viewController = LoginViewController.instance(
  with: GoogleAuthenticatorAdapter())
viewController.emailTextField.text = "user@example.com"
viewController.passwordTextField.text = "password"
viewController.login()
Auth succeeded: user@example.com, special-token-value

What should you be careful about?

The adapter pattern allows you to conform to a new protocol without changing an underlying type. This has the consequence of protecting against future changes against the underlying type, but it also makes your implementation harder to read and maintain.

Tutorial project

You’ll continue the previous chapter’s project, Coffee Quest, and create adapter classes to decouple the app from the Yelp SDK.

public var businesses: [YLPBusiness] = []
private let client = YLPClient(apiKey: YelpAPIKey)

import MapKit

public struct Business {
  var name: String
  var rating: Double
  var location: CLLocationCoordinate2D
}
import MapKit

public protocol BusinessSearchClient {
  func search(with coordinate: CLLocationCoordinate2D,
              term: String,
              limit: UInt,
              offset: UInt,
              success: @escaping (([Business]) -> Void),
              failure: @escaping ((Error?) -> Void))
}
import MapKit
import YelpAPI

// 1
extension YLPClient: BusinessSearchClient {
    
  public func search(with coordinate: CLLocationCoordinate2D,
                     term: String,
                     limit: UInt,
                     offset: UInt,
                     success: @escaping (([Business]) -> Void),
                     failure: @escaping ((Error?) -> Void)) {
    
    // 2
    let yelpCoordinate = YLPCoordinate(
      latitude: coordinate.latitude,
      longitude: coordinate.longitude)
      
    search(
      with: yelpCoordinate,
      term: term,
      limit: limit,
      offset: offset,
      sort: .bestMatched,
      completionHandler: { (searchResult, error) in
        
        // 3
        guard let searchResult = searchResult,
          error == nil else {
          failure(error)
          return
        }
        
        // 4
        let businesses =
          searchResult.businesses.adaptToBusinesses()
        success(businesses)
    })
  }
}

// 5
extension Array where Element: YLPBusiness {

  func adaptToBusinesses() -> [Business] {
  
    return compactMap { yelpBusiness in
      guard let yelpCoordinate =
        yelpBusiness.location.coordinate else {
        return nil
      }
      let coordinate = CLLocationCoordinate2D(
        latitude: yelpCoordinate.latitude,
        longitude: yelpCoordinate.longitude)
        
      return Business(name: yelpBusiness.name,
                      rating: yelpBusiness.rating,
                      location: coordinate)
    }
  }
}
private let client = YLPClient(apiKey: YelpAPIKey)
public var client: BusinessSearchClient = 
  YLPClient(apiKey: YelpAPIKey)
public var businesses: [YLPBusiness] = []
public var businesses: [Business] = []
// 1
client.search(
  with: mapView.userLocation.coordinate,
  term: "coffee",
  limit: 35, offset: 0,
  success: { [weak self] businesses in
    guard let self = self else { return }
    
    // 2
    self.businesses = businesses
    DispatchQueue.main.async {
      self.addAnnotations()
    }
  }, failure: { error in
  
    // 3
    print("Search failed: \(String(describing: error))")
})
public func createBusinessMapViewModel(
  for business: YLPBusiness) -> BusinessMapViewModel? {
  guard let yelpCoordinate =
    business.location.coordinate else {
    return nil
  }

  let coordinate = CLLocationCoordinate2D(
    latitude: yelpCoordinate.latitude,
    longitude: yelpCoordinate.longitude)
  public func createBusinessMapViewModel(
    for business: Business) -> BusinessMapViewModel {
    
    let coordinate = business.location
guard let viewModel = 
  annotationFactory.createBusinessMapViewModel(for: business) 
  else {
      continue
  }
let viewModel = 
  annotationFactory.createBusinessMapViewModel(for: business)

Key points

You learned about the adapter 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