Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

12. Adapter Pattern
Written by Jay Strawn

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.

For this example, you’ll adapt a third-party authentication service to work with an app’s internal authentication protocol. Add the following code, after Code Example:

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
}

Imagine GoogleAuthenticator is a third-party class that cannot be modified. Thereby, it is the legacy object. Of course, the actual Google authenticator would be a lot more complex; we’ve just named this one “Google” as an example and faked the networking call.

The login function returns a GoogleUser that has a string property called token. You might pass this token via a GET request like this:

  • https://api.example.com/items/id123?token=special-token-value

Or you might use this via Bearer authentication, such as a JSON Web Token (see https://jwt.io/). If you’re not familiar with these formats, that’s okay! They aren’t required knowledge for this chapter, but rather, they simply illustrate common use cases.

Next, add the following code to the end of the playground:

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

This is the authentication protocol for your app which acts as the new protocol. It requires an email and password. If login succeeds, it calls success with a User and Token. Otherwise, it calls failure with an Error.

The app will use this protocol instead of GoogleAuthenticator directly, and it gains many benefits by doing so. For example, you can easily support multiple authentication mechanisms – Google, Facebook and others – simply by having them all conform to the same protocol.

While you could extend GoogleAuthenticator to make it conform to AuthenticationService — which is also a form of the adapter pattern! — you can also create an Adapter class. Add the following code to the end of the playground to do so:

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

Here’s what this does:

  1. You create GoogleAuthenticationAdapter as the adapter between GoogleAuthenticationAdapter and AuthenticationService.

  2. You declare a private reference to GoogleAuthenticator, so it’s hidden from end consumers.

  3. You add the AuthenticationService login method as required by the protocol. Inside this method, you call Google’s login method to get a GoogleUser.

  4. If there’s an error, you call failure with it.

  5. Otherwise, you create user and token from the googleUser and call success.

By wrapping the GoogleAuthenticator like this, end consumers don’t need to interact with Google’s API directly. This protects against future changes. For example, if Google ever changed their API and it broke your app, you’d only need to fix it in one place: this adapter.

Add the following code to the end of the playground:

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

Here’s how this works:

  1. You first declare a new class for LoginViewController. It has an authService property and text fields for the e-mail and password. In a real view controller, you’d create the views in loadView or declare each as an @IBOutlet. For simplicity’s sake here, you set them to new UITextField instances.

  2. You then create a class method that instantiates a LoginViewController and sets authService.

  3. Lastly, you create a login method that calls authService.login with the e-mail and password from the text fields.

Next, add this code to try it out:

// MARK: - Example
let viewController = LoginViewController.instance(
  with: GoogleAuthenticatorAdapter())
viewController.emailTextField.text = "user@example.com"
viewController.passwordTextField.text = "password"
viewController.login()

You here create a new LoginViewController by passing GoogleAuthenticatorAdapter as the authService, set the text for the e-mail and password text fields and call login.

You should see this printed to the console:

Auth succeeded: user@example.com, special-token-value

If you wanted to support other APIs like Facebook login, you could easily make adapters for them as well and have the LoginViewController use them exactly the same way without requiring any code changes.

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.

Be careful about implementing the adapter pattern unless you recognize there’s a real possibility for change. If there isn’t, consider if it makes sense to use the underlying type directly.

Tutorial project

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

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. Then, open starter\CoffeeQuest\CoffeeQuest.xcworkspace (not .xcodeproj) in Xcode.

Note: If you opt to start fresh, then you’ll need to open up APIKeys.swift and add your Yelp API key. See Chapter 10, “Model-View-ViewModel Pattern” for instructions on how to generate this.

Open ViewController.swift, and you’ll see these two properties:

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

Thereby, CoffeeQuest directly depends on YLPBusiness and YLPClient, which are two classes provided by the Yelp SDK. Hence, the app is tightly coupled to the Yelp SDK.

If the SDK ever changed, you’d need to update the app in multiple places. This isn’t a big problem right now because the app is small. However, it’s likely to cause problems later if you continued developing the app and using the SDK directly in many places.

It’d be better if the app depended on an intermediary protocol and conformed to it in only one place. Sound familiar? This is exactly what the adapter pattern is meant to do!

You’ll first create new groups and files to organize your new types.

Right click on the CoffeeQuest group, select New Group and name it Adapters. Repeat this to create a second group named Models and a third group named Protocols.

Next, right click on Adapters and select New File…. Select iOS ▸ Swift File and click Next. Call it YLPClient+BusinessSearchClient.swift and click Create.

Repeat this proccess to create a new filed called Business.swift under Models and BusinessSearchClient.swift under Protocols.

Lastly, right click on CoffeeQuest and select Sort by Name.

Your file hierarchy should now look like this:

You’ll need to use Business in the protocol and adapter, so you’ll create this type first. Replace the contents of Business.swift with the following:

import MapKit

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

You’ll use this model instead of YLPBusiness directly. By doing so, you could later easily use other APIs like Google Places or MapKit and map their outputs to Business objects.

Next, replace the contents of BusinessSearchClient.swift with the following:

import MapKit

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

You’ll use this protocol instead of YLPSearch.

How exactly will you use YLPSearch? That’s where the adapter comes in. Finally, replace the contents of YLPClient+BusinessSearchClient.swift with the following:

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

Here’s how this works:

  1. You extend YLPClient to conform to BusinessSearchClient. This requires you to implement search(with:term:limit:offset:success:failure:).

  2. To perform a search, you convert the passed-in CLLocationCoordinate2D to a YLPCoordinate and call search on YLPClient.

  3. Within the completionHandler, you verify there’s a searchResult and there’s not an error. Otherwise, you call the failure with the error.

  4. In the success case, you call adaptToBusinesses() on the searchResult.businesses to convert the YLPBusiness array into a Business array.

  5. You extend Array where Element is YLPBusiness. This allows you to create a convenience method to convert a YLPBusiness array into a Business array.

You’re now ready to actually use BusinessSearchClient and Business! Open ViewController.swift and replace this line

private let client = YLPClient(apiKey: YelpAPIKey)

With this instead, igorning the compiler error for now:

public var client: BusinessSearchClient = 
  YLPClient(apiKey: YelpAPIKey)

It’s subtle, but there are three significant changes here:

  1. You changed private to public. Thereby, if you later wanted to use a different type of BusinessSearchClient, you’d be able to get a reference to the view controller and set its client.

  2. You explicitly declare the property type as BusinessSearchClient. This ensures that the compiler doesn’t automatically infer this to be YLPClient.

  3. You set this property to an instance of YLPClient by default.

By making this change, you’ve actually decoupled the view controller from YLPClient! It now depends on BusinessSearchClient, and you can easily replace this with any other conforming type.

Next, replace the following line:

public var businesses: [YLPBusiness] = []

With the following:

public var businesses: [Business] = []

This is another step towards decoupling from from the Yelp SDK. However, you now have to fix the resulting compiler errors from these changes.

First, replace the contents of searchForBusinesses() with the following:

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

Here’s how this works:

  1. You update the callsight to use the method declared on BusinessSearchClient, instead of the one from YLPClient.

  2. If the search succeeds, you set self.businesses to the fetched businesses. You then dispatch to the main queue to add the annotations to the map.

  3. If the search fails, you simply print the error to the console.

You next need to update annotationFactory.createBusinessMapViewModel. This method currently expects a YLPBusiness for the input. Instead, you need to change this to accept a Business.

Open AnnotationFactory.swift, and replace these lines:

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)

With the following:

  public func createBusinessMapViewModel(
    for business: Business) -> BusinessMapViewModel {
    
    let coordinate = business.location

There’s three small changes here:

  1. You changed the input parameter’s type to Business.

  2. You changed the return type to BusinessMapViewModel instead of an optional, BusinessMapViewModel?.

  3. You get the coordinate from the business.location, which doesn’t require a guard because it’s not an optional type.

There’s just one final change you need to make to get the project to compile again. Open ViewController.swift, and you’ll find there’s a compiler error within addAnnoations().

Replace this code:

guard let viewModel = 
  annotationFactory.createBusinessMapViewModel(for: business) 
  else {
      continue
  }

With the following instead:

let viewModel = 
  annotationFactory.createBusinessMapViewModel(for: business)

This removes the guard because the return type is no longer optional.

Now that you’ve adapted your code, build and run to confirm everything works as expected.

Key points

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

  • The adapter pattern is useful when working with classes from third-party libraries that cannot be modified. You can use protocols to have them work with the project’s custom classes.

  • To use an adapter, you can either extend the legacy object, or make a new adapter class.

  • The adapter pattern allows you to reuse a class even if it lacks required components or has incompatible components with required objects.

  • In A Briefer History of Time, Steven Hawking said, “Intelligence is the ability to adapt to change.” Maybe he wasn’t talking about the adapter pattern exactly, but this idea is an important component in this pattern and many others: plan ahead for future changes.

Coffee Quest is getting better with every refactor! Once again, not much has changed in the app from a visual perspective, but now it will be so much easier to add other APIs and have them work seamlessly with your Business objects.

In the next chapter, you’ll learn about the iterator pattern. The current mechanism of iterating through businesses in the view controller is less than ideal. You’ll learn how to extend classes to make them iterable and easier to manage.

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.