scribd.github.io/_posts/2018-10-24-dependency-injec...

26 KiB
Raw Permalink Blame History

layout title author tags team
post Dependency injection tutorial with Weaver on iOS (part 1) theo
swift
weaver
dependency injection
di-series
iOS

In this tutorial youre going to explore how Dependency Injection (DI) and DI Containers can be used to develop robust iOS applications. To do so, Ill explain step by step how Weavers sample application was written and why.

**Dependency Injection (DI) basically means “giving an object its instance variables”* ¹.*

Its a way to organize the code, so that the logic of an object can delegate part of its work to other objects (dependencies) without being responsible for their instantiation. For that reason, dependencies can also be injected as abstract objects (protocols). It turns out that this loose coupling between objects makes the code more modular and flexible, and thus, a lot easier to unit test.

While DI can be implemented manually, object initializers can easily become very complex, encouraging developers to use anti-patterns like singletons. Thats where Weaver can help, by generating the necessary boilerplate code to inject dependencies into Swift types and ensure a clean dependency graph.

At the end of this tutorial, youll get the following application up and running.

Weaver’s Sample App DemoWeavers Sample App Demo

1. Under the hood

Before jumping into the code, lets see what Weaver does under the hood:

  1. Scans the Swift code for annotations.
  2. Assembles the collected annotations to create a dependency graph.
  3. Validates the graph, looking for potential dependency cycles and unresolvable dependencies.
  4. Generates the boilerplate code.

Note that all these steps happen at compile time, thus, if any of them fail, it would stop the projects compilation.

2. Installation

The easiest way to install Weaver is with Homebrew.

If you dont have it installed on your machine, run the following command:

$ /usr/bin/ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Then install Weaver like so:

$ brew install weaver

If you dont want to use Homebrew, you can also install Weaver manually as explained in the readme of the project.

3. Getting started

First of all, lets create a project called Sample.

Xcode -> File -> New -> ProjectXcode -> File -> New -> Project

Xcode project creation windowXcode project creation window

Create the directory Generatedat the root of the project. Thats where Weaver will generate the boilerplate code. Make sure to create a directory and not a group.

The project hierarchy should now look like this:

Directory hierarchyDirectory hierarchy

Since Weaver works at compile time, it needs to be executed before the compilation happens. The easiest way to do that is to add a build phase to the project.

Lets hit the button Sample -> Sample -> Build Phases -> + -> New Run Script Phase as shown in the following screenshot.

Sample -> Sample -> Build Phases -> + -> New Run ScriptSample -> Sample -> Build Phases -> + -> New Run Script

Rename it toWeaver and then, in the Shell window, add the following command line:

weaver swift --project-path $PROJECT_DIR/$PROJECT_NAME --output-path Generated --input-path "*.swift"

This command searches for all the Swift source files in the project, gives them as arguments to Weaver so that it can analyze the code and generate the boilerplate under the directory Sample/Generated.

**Make sure to move this build phase above the compilation phase. **If not, it will execute post compilation, which will most likely prevent the project from compiling correctly.

The list of build phases should now look like this:

List of build phasesList of build phases

At this point, you should be able to hit cmd+b and the project should compile correctly. If you do that, youll probably notice that nothing actually gets written in the Generated directory. Its because we havent started to inject dependencies yet.

4. First dependency injection

Lets talk a bit about the application were trying to build here. It will be composed of:

  • a screen with a list of movies (HomeViewController)

  • a screen with the details of a movie (MovieViewController)

  • a screen with the reviews of a movie (ReviewViewController)

The HomeViewController will be the root controller of the app, the MovieViewController will show when tapping on a movie cell. Then from the MovieViewController, users will be able to tap the movie cover image to show the ReviewViewController.

Well start by creating the first two controllers, then well take care of fetching and injecting the movies.

First thing first, lets rewrite the AppDelegate.

AppDelegate.swift on GistAppDelegate.swift on Gist

Before trying to show movies on the screen, lets define what a movie is.

Movie.swift on GistMovie.swift on Gist

Note thatMovie implements Decodable because it will need to be built from a JSON payload later on.

Now that we have defined the Movie class, lets create the HomeViewController class.

HomeViewController.swift on GistHomeViewController.swift on Gist

Nothing new here, this is only a regular controller with a UITableView implementing UITableViewDelegate/UITableViewDataSource.

Now, lets come back to the AppDelegate. Of course, we could build a HomeViewController manually and set it to the window, but that wouldnt work well with Weaver. What we will do instead, is use Weaver to register an instance of HomeViewController in the AppDelegate like so.

AppDelegate.swift on GistAppDelegate.swift on Gist

In this new version of the AppDelegate, we used // weaver: homeViewController = HomeViewController <- UIViewController to register an instance of HomeViewController in AppDelegate.

By default, HomeViewController is instantiated once, then reused. Well see how to personalize this behavior later on.

Also notice we have instantiated the AppDelegates DI container, which was generated by Weaver. Finally, we attached the instance of HomeViewController to the window.

By accessing the HomeViewController with dependencies.homeViewController, the DI container automatically creates an instance of HomeViewController, and since we registered it with the scope container, this instance will be created only once and then reused afterwards.

At this point if you try to compile, youll notice that the compiler fails because it cant find the protocol AppDelegateDependencyResolver nor the class AppDelegateDependencyContainer. This is because we need to add the file Weaver.AppDelegate.swift Weaver generated for us to the project. It should be located into the Generated directory. Once the file is added, the project should compile and you should be able to see an empty UITableView at the screen.

Thats it, you just injected your first dependency.

5. Injection with parameters

Next well inject some movies intoHomeViewController. To do so, I propose to write a class called MovieManager which will basically fetch movies from the The Movie DB API, then inject it intoHomeViewController.

For this sample, well use the TMDB Discover endpoint which returns a JSON payload containing a page of movies.

To be able to decode a page of movies, lets declare a struct Page first.

Page.swift on GistPage.swift on Gist

This struct takes a generic Model as an argument, which has to implement Decodable, making it able to contain and decode any type of item.

MovieManager.swift on GistMovieManager.swift on Gist

In the MovieManager class above, we declared the method getDiscoverMovies(_:) which completes with aPage. We used a shared instance of the URLSession to create a task, which hits https://api.themoviedb.org/3/discover/movie?api_key=1a6eb1225335bbb37278527537d28a5d. When the task gets a response, it parses it and turns it into a Page and completes. This code is far from perfection but it does the job. Ill talk about ways of improving it later on.

Now that we have a MovieManager, we can inject it into HomeViewController.

HomeViewController.swift on GistHomeViewController.swift on Gist

The same way we injected HomeViewController intoAppDelegate, we can use Weaver to inject MovieManager into HomeViewController.

Note that this time, rather than instantiating the dependency container directly, we wrote a new initializer init(injecting:) and stored it. The reason for this is because we want to be able to resolve shared dependencies, and to do so, we need to get the dependency container from the call site. Notice how this is different from the special case of the AppDelegate which is the root class of the application, thus, doesnt have any call site, leaving us no other choice but to instanciate the dependency container manually.

Also note how we store the dependency container privately. Unless you want to expose all of the dependencies to the outside world, Id say that its best practice to keep it private. Ill explain later in this post how to share a dependency between classes with Weaver.

Once the dependencies are injected properly, we can call dependencies.movieManager.getDiscoverMovies(_:) and load the movies into UITableView.

And of course, dont forget to add the file Weaver.MovieViewController.swift in the project so that it compiles correctly.

Nothing really new so far. Lets make this sample a bit more complex and add a MovieViewController which will show the details of a movie. To do so, well need to instantiate MovieViewController with a Movie. Lets jump to the code to see how Weaver can help us do that.

MovieViewController.swift on GistMovieViewController.swift on Gist

You probably noticed an annotation I never mentioned before; // weaver: movie <= Movie

It makes Weaver aware that resolving MovieViewController requires a movie.

HomeViewController.swift on GistHomeViewController.swift on Gist

Here we use the annotation // weaver: movieController = MovieViewController <- UIViewController, which tells weaver to generate an accessor to movieController from the HomeViewControllerDependencyContainer. This way, when the user taps on the cell, we can use the method dependencies.movieController(movie:) to build a new MovieViewController and push it to the stack.

Then we use a new annotation // weaver: movieController.scope = .transient in oder to personalize the default instantiation behavior. The transient scope tells Weaver to create a new instance for each resolution, which means that a new MovieViewController is pushed each time the user taps a cell. You can find the different available scopes in the documentation.

Also note that using any scope other than transient to resolve a dependency that takes arguments doesnt make much sense. Weaver allows it, but be aware that the second time the dependency will get resolved, the argument(s) will be ignored as your initial object has become shared.

6. Custom builders

So far, the MovieViewController isnt great. It only shows text. We should show an image of the movies cover to make it feel a bit more real.

To do so, well need to fetch an image from TMDB, for example, from the following url: https://image.tmdb.org/t/p/w1280/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg.

This will be the purpose of the following ImageManager class.

ImageManager.swift on GistImageManager.swift on Gist

So far, this manager is not so different from MovieManager. Its also not quite correct in terms of dependency injection because it uses a singleton of URLSession of having it injected. Remember when I told you I would cover ways to improveMovieManager? Well, thats one of them.

The following injects URLSession the same way we injected HomeViewController in AppDelegate.

ImageManager.swift on GistImageManager.swift on Gist

If you try to compile and run the application at this point, youll see that it crashes because URLSession needs to be build with a configuration parameter to work properly. To make this work, Weaver needs a custom URLSession builder.

ImageManager on GistImageManager on Gist

In the code above, I added the annotation: // weaver: urlSession.builder = URLSession.make to make Weaver know that we want to implement our own URLSession builder. Then I wrote the method make() -> URLSession which is automatically called by the dependency container when resolving theURLSession.

By the way, MovieManager having the same issue we could do the same thing for it as well.

ImageManager is now ready to be used in MovieViewController, which is what youll see in the following code.

MovieViewController.swift on GistMovieViewController.swift on Gist

7. Shared dependencies

Although all of this works fine, its still a bit too much code to write to only inject URLSession,which will most likely be used a lot throughout the project. Itd be better to have to write this make(_:) method only once.

What if we could share one URLSession instance which bothImageManager and MovieManager could use? Lets see how Weaver can help with that.

First of all, we need to declare a reference of URLSession in both ImageManager and MovieManager.

Previously we had typed: // weaver: urlSession = URLSession and were changing it to: // weaver: urlSession <- URLSession.

If you try to compile this, youll see that Weaver complains about the fact that urlSession isnt resolvable.

To resolve a reference like this one, Weaver recursively goes through all of the direct ancestors of ImageManager and checks if theres an instance of URLSession registered for each one of them. If theres one ancestor which doesnt, it explores its ancestors the same way and so on until the instance is found or theres no more ancestors to explore. In this case, Weavers hitting the root (AppDelegate), then fails because theres no more ancestors to explore and were still missing an instance of URLSession.

To fix this we need to register a shared instance of URLSession in a common ancestor of MovieManager and ImageManager.

Dependency GraphDependency Graph

Looking at the dependency graph above, we can easily deduce that HomeViewController and AppDelegate are the only two common ancestors of MovieManager and ImageManager. For conveniency, well pick AppDelegate.

AppDelegate.swift on GistAppDelegate.swift on Gist

Note how we used the new scope container here. This scope does the same thing than the default one, but also exposes the dependency to the adjacent objects in the dependency graph. In this example, since we exposed urlSession from AppDelegate, the whole graph has access to it. If we had exposed it from MovieViewController, only ImageManager would have had access to it.

Finally, we can declare a reference of URLSession in MovieManager so it uses the same instance than ImageManager.

MovieManager.swift on GistMovieManager.swift on Gist

This is a very common scenario which youll see a lot if you start using Weaver in your project. In fact, this shared instance of urlSession shows all the advantages of a singleton, without the disadvantages. Indeed, since we registered it at the root, its accessible throughout the entire app but its still injected, and thus, easily override-able and testable. Ill explain how in an another tutorial.

8. ObjC compatibility

So far we saw how Weaver smoothly integrates into a Swift codebase, but what if we were working on a project containing classes written in ObjC?

Once again, Weaver has got us covered. Lets see how we implement the last controller of this sample project; ReviewViewController.

This controller will show up when tapping on a movie cover image in MovieViewController. It will be written in ObjC, and will basically be a UITableView of reviews.

And to get a movies reviews, we need aReviewManager. I promise, this will be the last.

ReviewManager uses the TMDB Review endpoint. For example, itll hit the URL; https://api.themoviedb.org/3/movie/550/reviews?api_key=1a6eb1225335bbb37278527537d28a5d&language=en-US&page=1 and get the following JSON payload as a response.

Example of reviews of a movieExample of reviews of a movie

First things first, lets declare a new model class which well call Review. Since this class needs to be used in ObjC, it needs to be annotated accordingly.

Review.swift on GistReview.swift on Gist

Since these reviews come in pages, we also need to write an ObjC friendly ReviewPage.

ReviewPage.swift on GistReviewPage.swift on Gist

And now lets write our ObjC friendly ReviewManager:

ReviewManager.swift on GistReviewManager.swift on Gist

Note that we reused the same urlSession reference than in MovieManager and ImageManager.

We also used the new annotation // weaver: self.isIsolated = true which tells Weaver that ReviewManager isnt in the dependency graph yet. As soon as Weaver knows that, it avoids performing dependency resolution checks on ReviewManager. Otherwise, it would fail on the urlSession reference resolution. This is only a temporary solution until ReviewManager can be introduced in the dependency graph.

Finally, we can write WSReviewViewController in ObjC.

WSReviewViewController.m on Gist & ReviewTableViewCell.swiftWSReviewViewController.m on Gist & ReviewTableViewCell.swift

For now, theres no injection and WSReviewController only shows an empty UITableView.

Now we have a problem because Weaver cant parse ObjC source code, thus, adding annotations to this controller would have no effect. But what we can do is extend WSReviewController in a swift file which Weaver can understand.

WSReviewViewController+Injectable.swift on GistWSReviewViewController+Injectable.swift on Gist

By extending WSReviewViewControllerObjCDependencyInjectable, we tell Weaver to consider this extension as a regular class, making it generate the protocolWSReviewViewControllerDependencyResolver.

All is left to do is write an initializer in WSReviewViewController that takes a id and stores it, then uses it to resolve our ReviewManager.

WSReviewViewController.h on GistWSReviewViewController.h on Gist

WSReviewViewController.m on GistWSReviewViewController.m on Gist

Now we can show WSReviewViewController when a user taps on the MovieViewController movie cover image. Also, since we are registering WSReviewViewController, it is now part of the dependency graph, meaning we have to get rid of the annotation // weaver: self.isIsolated = true in ReviewManager and WSReviewViewController+Injectable.

MovieViewController.swift on GistMovieViewController.swift on Gist

Here we registered WSReviewViewController with a transient registration annotation and used it with the DI containers method func reviewController(movieID:).

Note how the fact that we wrote our controller in ObjC doesnt impact the way its injected. This means that the day you want to rewrite it in Swift you wont have to rewrite any Weaver annotation in your codebase.

Conclusion

Thats all for the first part of this tutorial in which you learned how to:

  • Write injectable Swift classes

  • Inject a dependency by registration

  • Inject a dependency by reference

  • Inject a dependency as a parameter

  • Use a custom dependency builder

  • Share a dependency throughout the codebase

  • Write injectable ObjC classes

While exploring all of the above, we rewrote a slightly simplified version of the Weavers sample app.

In the second part we will see how we can use Weaver to write **clean and flexible unit tests.**

I hope to see you soon, and of course, for any question or suggestion feel free to comment or reach me on Twitter @thrupin.

Thanks for reading!

Cheers

If you want to work on changing how the world reads, come join us! www.scribd.com/careers