Weaver
Dependency Injection framework for Swift (iOS/macOS/Linux)
Install / Use
/learn @scribd/WeaverREADME
| :exclamation: Deprecation Notice | |:-| |We want to express our sincere gratitude for your support and contributions to the Weaver open source project. As we are no longer using this technology internally, we have come to the decision to archive the Weaver repository. While we won't be providing further updates or support, the existing code and resources will remain accessible for your reference. We encourage anyone interested to fork the repository and continue the project's legacy independently. Thank you for being a part of this journey and for your patience and understanding.|
Features
- [x] Dependency declaration via property wrappers or comments
- [x] DI Containers auto-generation
- [x] Dependency Graph compile time validation
- [x] ObjC support
- [x] Non-optional dependency resolution
- [x] Type safety
- [x] Injection with arguments
- [x] Registration Scopes
- [x] DI Container hierarchy
- [x] Thread safe
Talks
Tutorials
If you're looking for a step by step tutorial, check out these links.
- Part 1 - Basics
- Part 2 - Unit Testing
- Part 3 - Multi target application (coming soon)
Dependency Injection
Dependency Injection basically means "giving an object its instance variables" ¹. It seems like it's not such a big deal, but as soon as a project gets bigger, it gets tricky. Initializers become too complex, passing down dependencies through several layers becomes time consuming and just figuring out where to get a dependency from can be hard enough to give up and finally use a singleton.
However, Dependency Injection is a fundamental aspect of software architecture, and there is no good reason not to do it properly. That's where Weaver can help.
What is Weaver?
Weaver is a declarative, easy-to-use and safe Dependency Injection framework for Swift.
- Declarative because it allows developers to declare dependencies via annotations directly in the Swift code.
- Easy-to-use because it generates the necessary boilerplate code to inject dependencies into Swift types.
- Safe because it's all happening at compile time. If it compiles, it works.
How does Weaver work?
|-> validate() -> valid/invalid
swift files -> scan() -> [Token] -> parse() -> AST -> link() -> Graph -> |
|-> generate() -> source code
Weaver scans the Swift sources of the project, looking for annotations, and generates an AST (abstract syntax tree). It uses SourceKitten which is backed by Apple's SourceKit.
The AST then goes through a linking phase, which outputs a dependency graph.
Some safety checks are then performed on the dependency graph in order to ensure that the generated code won't crash at runtime. Issues are friendly reported in Xcode to make their correction easier.
Finally, Weaver generates the boilerplate code which can directly be used to make the dependency injections happen.
Installation
(1) - Weaver command
Weaver can be installed using Homebrew, CocodaPods or manually.
Binary form
Download the latest release with the prebuilt binary from release tab. Unzip the archive into the desired destination and run bin/weaver
Homebrew
$ brew install weaver
CocoaPods
Add the following to your Podfile:
pod 'WeaverDI'
This will download the Weaver binaries and dependencies in Pods/ during your next pod install execution and will allow you to invoke it via ${PODS_ROOT}/WeaverDI/weaver/bin/weaver in your Script Build Phases.
This is the best way to install a specific version of Weaver since Homebrew cannot automatically install a specific version.
Mint
To use Weaver via Mint, prefix the normal usage with mint run scribd/Weaver like so:
mint run scribd/Weaver version
To use a specific version of Weaver, add the release tag like so:
mint run scribd/Weaver@1.0.7 version
Building from source
Download the latest release source code from the release tab or clone the repository.
In the project directory, run brew update && brew bundle && make install to build and install the command line tool.
Check installation
Run the following to check if Weaver has been installed correctly.
$ weaver swift --help
Usage:
$ weaver swift
Options:
--project-path - Project's directory.
--config-path - Configuration path.
--main-output-path - Where the swift code gets generated.
--tests-output-path - Where the test helpers gets generated.
--input-path - Paths to input files.
--ignored-path - Paths to ignore.
--cache-path - Where the cache gets stored.
--recursive-off
--tests - Activates the test helpers' generation.
--testable-imports - Modules to imports in the test helpers.
--swiftlint-disable-all - Disables all swiftlint rules.
(2) - Weaver build phase
In Xcode, add the following command to a command line build phase:
weaver swift --project-path $PROJECT_DIR/$PROJECT_NAME --main-output-path output/relative/path
Important - Move this build phase above the Compile Source phase so that Weaver can generate the boilerplate code before compilation happens.
Basic Usage
For a more complete usage example, please check out the sample project.
Let's implement a simple app displaying a list of movies. It will be composed of three noticeable objects:
AppDelegatewhere the dependencies are registered.MovieManagerproviding the movies.MoviesViewControllershowing a list of movies at the screen.
Let's get into the code.
AppDelegate with comment annotations:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private let dependencies = MainDependencyContainer.appDelegateDependencyResolver()
// weaver: movieManager = MovieManager <- MovieManaging
// weaver: movieManager.scope = .container
// weaver: moviesViewController = MoviesViewController <- UIViewController
// weaver: moviesViewController.scope = .container
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow()
let rootViewController = dependencies.moviesViewController
window?.rootViewController = UINavigationController(rootViewController: rootViewController)
window?.makeKeyAndVisible()
return true
}
}
AppDelegate registers two dependencies:
// weaver: movieManager = MovieManager <- MovieManaging// weaver: moviesViewController = MoviesViewController <- UIViewController
These dependencies are made accessible to any object built from AppDelegate because their scope is set to container:
// weaver: movieManager.scope = .container// weaver: moviesViewController.scope = .container
A dependency registration automatically generates the registration code and one accessor in AppDelegateDependencyContainer, which is why the rootViewController can be built:
let rootViewController = dependencies.moviesViewController.
AppDelegate with property wrapper annotations:
Since Weaver 1.0.1, you can use property wrappers instead of annotations in comments.
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// Must be declared first!
private let dependencies = MainDependencyContainer.appDelegateDependencyResolver()
@Weaver(.registration, type: MovieManager.self, scope: .container)
private var movieManager: MovieManaging
@Weaver(.registration, type: MoviesViewController.self, scope: .container)
private var moviesViewController: UIViewController
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow()
window?.rootViewController = UINavigationController(rootViewController: moviesViewController)
window?.makeKeyAndVisible()
return true
}
}
-
Note how dependencies can be accessed from the
selfinstance directly. -
Also note that the dependencies object must be declared and created prior to any other Weaver annotation. Not doing so would immediately crash the application.
-
It is possible to use comment an
