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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Swinject Tutorial for iOS: Getting Started
30 mins
- Why Dependency Injection?
- Getting Started
- DI and Coupling – Oh my!
- Networking and Parsing
- Formatting
- Extracting Dependencies
- Extracting Networking Logic
- Extracting Parsing Logic
- Testing Bitcoin Adventurer
- Prior to Writing the Tests
- Writing Your First Swinject Test
- Improving Tests with Autoregister
- Prior to Writing the Tests
- Autoregistered Tests
- Simulating Networking in Tests
- Prior to Writing the Tests
- Writing Your First Complex Test
- The Final Test
- Dependency Injection Outside of Tests
- Extending SwinjectStoryboard
- Swinject & BitcoinViewController
- Where to Go From Here?
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:
-
This defines a
Networking
protocol that has one method:
request(from:completion:)
. Itscompletion
block returns either aData
object representing the body of the response, or anError
object. -
You then create a concrete implementation of this protocol, named
HTTPNetworking
. -
request(from:completion:)
creates aURL
from the providedEndpoint
. It uses thisURL
to create aURLRequest
, which is then used to create aURLSessionDataTask
(phew!). This task is then executed, firing an HTTP request and returning its result via the providedCompletionHandler
. -
createRequest(from:)
is called from withinrequest(from:completion:)
. It takes aURL
and returns aURLRequest
. -
createDataTask(from:completion:)
is called bycreateRequest(from:)
. It takes aURLRequest
and returns aURLSessionDataTask
.
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 IBOutlet
s:
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
}
}
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:
- You register the
Currency
type to always returnUSD
- You register the
CryptoCurrency
type to always returnBTC
- This is where things get interesting: you register the
Price
type which in itself has two dependencies. To create these dependencies, ask the providedresolver
to create them by usingresolve(_:)
. - Similarly to the previous registration, you register the
PriceResponse
type and resolve aPrice
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
.