Swinject Tutorial for iOS: Getting Started

In this tutorial, you will explore Dependency Injection (DI) through Swinject, a Dependency Injection framework written in Swift. Dependency Injection is an approach to organizing code so that its dependencies are provided by a different object, instead of by itself. By Gemma Barlow.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Extracting Dependencies

Start by creating a new folder named Dependencies. This will hold all of the various logic pieces you’ll be extracting throughout the rest of this tutorial.

Right-click on the Bitcoin Adventurer folder and select New Group. Then set its name to Dependencies.

Are you ready to start extracting the different logic pieces and make the code testable, robust and beautiful? Thought so! :]

Extracting Networking Logic

While the Dependencies folder is selected, go to File\New\File and create a Swift file named HTTPNetworking.swift. Make sure the Bitcoin Adventurer target is selected.

Now, add the following code to the newly created file:

// 1.
protocol Networking {
  typealias CompletionHandler = (Data?, Swift.Error?) -> Void
  
  func request(from: Endpoint, completion: @escaping CompletionHandler)
}

// 2.
struct HTTPNetworking: Networking {
  // 3.
  func request(from: Endpoint, completion: @escaping CompletionHandler) {
    guard let url = URL(string: from.path) else { return }
    let request = createRequest(from: url)
    let task = createDataTask(from: request, completion: completion)
    task.resume()
  }
  
  // 4.
  private func createRequest(from url: URL) -> URLRequest {
    var request = URLRequest(url: url)
    request.cachePolicy = .reloadIgnoringCacheData
    return request
  }
  
  // 5.
  private func createDataTask(from request: URLRequest,
                              completion: @escaping CompletionHandler) -> URLSessionDataTask {
    return URLSession.shared.dataTask(with: request) { data, httpResponse, error in
      completion(data, error)
    }
  }
}

Time to review the code you just added:

  1. This defines a Networking protocol that has one method:
    request(from:completion:). Its completion block returns either a Data object representing the body of the response, or an Error object.
  2. You then create a concrete implementation of this protocol, named HTTPNetworking.
  3. request(from:completion:) creates a URL from the provided Endpoint. It uses this URL to create a URLRequest, which is then used to create a URLSessionDataTask (phew!). This task is then executed, firing an HTTP request and returning its result via the provided CompletionHandler.
  4. createRequest(from:) is called from within request(from:completion:). It takes a URL and returns a URLRequest.
  5. createDataTask(from:completion:) is called by createRequest(from:). It takes a URLRequest and returns a URLSessionDataTask.

Does this logic ring any bells?

If you said “yes” that’s because it’s similar to the first half of requestPrice() in BitcoinViewController.

OK, time to adjust that code so it uses the shiny new HTTPNetworking class instead. Add the following to the top of BitcoinViewController, below the three IBOutlets:

let networking = HTTPNetworking()

Finally, refactor requestPrice() by replacing its entire contents with:

networking.request(from: Coinbase.bitcoin) { data, error in
  // 1. Check for errors
  if let error = error {
    print("Error received requesting Bitcoin price: \(error.localizedDescription)")
    return
  }

  // 2. Parse the returned information
  let decoder = JSONDecoder()
  guard let data = data,
        let response = try? decoder.decode(PriceResponse.self, from: data)
  else { return }

  print("Price returned: \(response.data.amount)")

  // 3. Update the UI with the parsed PriceResponse
  DispatchQueue.main.async { [weak self] in
    self?.updateLabel(price: response.data)
  }
}

Press Command-R to build and run the app; things should work just as they did before, but now the networking is more contained and decoupled than it was before.

Excellent! You successfully moved logic out of BitcoinViewController and into a more cohesive HTTPNetworking object. However, to achieve proper Dependency Injection, more decoupling is required.

Extracting Parsing Logic

Under the same Dependencies folder, go to File\New\File and create a new Swift file named BitcoinPriceFetcher.swift. Give it the following implementation:

protocol PriceFetcher {
  func fetch(response: @escaping (PriceResponse?) -> Void)
}

struct BitcoinPriceFetcher: PriceFetcher {
  let networking: Networking

  // 1. Initialize the fetcher with a networking object
  init(networking: Networking) {
    self.networking = networking
  }

  // 2. Fetch data, returning a PriceResponse object if successful
  func fetch(response: @escaping (PriceResponse?) -> Void) {
    networking.request(from: Coinbase.bitcoin) { data, error in
      // Log errors if we receive any, and abort.
      if let error = error {
        print("Error received requesting Bitcoin price: \(error.localizedDescription)")
        response(nil)
      }

      // Parse data into a model object.
      let decoded = self.decodeJSON(type: PriceResponse.self, from: data)
      if let decoded = decoded {
        print("Price returned: \(decoded.data.amount)")
      }
      response(decoded)
    }
  }

  // 3. Decode JSON into an object of type 'T'
  private func decodeJSON<T: Decodable>(type: T.Type, from: Data?) -> T? {
    let decoder = JSONDecoder()
    guard let data = from,
          let response = try? decoder.decode(type.self, from: data) else { return nil }

    return response
  }
}
Note: The PriceFetcher protocol defines a single method: one that performs a fetch and returns a PriceResponse object. This “fetch” can occur from any data source, not necessarily an HTTP request. This will become an important characteristic of this protocol when you get to writing the unit tests. This fetcher makes use of the newly-created Networking protocol.

You now have an even more specific abstraction to fetch Bitcoin prices that, itself, uses the Networking protocol internally. It’s time to refactor BitcoinViewController once more to use it.

Replace:

let networking = HTTPNetworking()

With:

let fetcher = BitcoinPriceFetcher(networking: HTTPNetworking())

Then, completely replace requestPrice() with:

private func requestPrice() {
  fetcher.fetch { response in
    guard let response = response else { return }

    DispatchQueue.main.async { [weak self] in
      self?.updateLabel(price: response.data)
    }
  }
}

The code is now more readable and concise! It uses the bare minimum boilerplate code, while the external BitcoinPriceFetcher dependency does the “heavy lifting”.

Finally, build and run using Command-R to make sure the app continues to operate correctly.

Congratulations, you just improved the quality of your code! You now have it using Dependency Injection: the HTTPNetworking dependency is provided to BitcoinPriceFetcher upon initialization. Shorty, you’ll replace this with a Swinject-based approach.

Next, you’ll look at unit testing, and then you’ll return to Bitcoin Adventurer to further decouple the code.

Testing Bitcoin Adventurer

Some unit tests have already been set up for you in the Bitcoin Adventurer Tests target and are ready for you to fill in. Run them by pressing Command-U. Look at the Test navigator; all of the tests have failed:

Press Command-Shift-O and search for the file named BasicTests.swift. Note that Swinject was aleady imported at the top of this file.

Prior to Writing the Tests

Swinject relies on the use of a Container object, which maps all of the object’s dependencies when it’s tested. Prior to the test suite being run, dependencies will be registered within the container, and then resolved from the container as needed. This becomes especially useful where many different object types are required, or dependencies between objects are complex.

A container has been provided at the top of the BasicTests class, ready for use in this test suite.

At the end of setUp(), add the following lines to define the relationships between PriceResponse, Price, Currency and Cryptocurrency:

// 1
container.register(Currency.self) { _ in .USD }
container.register(CryptoCurrency.self) { _ in .BTC }

// 2
container.register(Price.self) { resolver in
  let crypto = resolver.resolve(CryptoCurrency.self)!
  let currency = resolver.resolve(Currency.self)!
  return Price(base: crypto, amount: "999456", currency: currency)
}

// 3
container.register(PriceResponse.self) { resolver in
  let price = resolver.resolve(Price.self)!
  return PriceResponse(data: price, warnings: nil)
}

This piece of code uses the container’s register() method to register how specific dependencies are created for this unit test. It accepts the expected type as an argument and a closure with a single argument — a resolver — that creates an instance of that type. Later, when your container is asked to “resolve” these dependencies, it will use these registrations to create them as needed.

Time to go through your first-ever Swinject code:

  1. You register the Currency type to always return USD
  2. You register the CryptoCurrency type to always return BTC
  3. This is where things get interesting: you register the Price type which in itself has two dependencies. To create these dependencies, ask the provided resolver to create them by using resolve(_:).
  4. Similarly to the previous registration, you register the PriceResponse type and resolve a Price dependency needed to create it.

At this point, your tests will still fail, but your testable dependencies are now correctly registered in the Swinject Container.

Gemma Barlow

Contributors

Gemma Barlow

Author

Shai Mishali

Tech Editor

Vladyslav Mytskaniuk

Illustrator

Marin Bencevic

Final Pass Editor

Richard Critz

Team Lead

Over 300 content creators. Join our team.