{"id":13465667,"url":"https://github.com/devxoul/Pure","last_synced_at":"2025-03-25T16:32:36.621Z","repository":{"id":28636352,"uuid":"118639791","full_name":"devxoul/Pure","owner":"devxoul","description":"Pure DI in Swift","archived":false,"fork":false,"pushed_at":"2022-07-22T01:14:16.000Z","size":60,"stargazers_count":381,"open_issues_count":8,"forks_count":18,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-03-22T12:06:20.256Z","etag":null,"topics":["composition-root","dependency-injection","pure-di","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devxoul.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-01-23T16:49:21.000Z","updated_at":"2025-01-20T14:35:23.000Z","dependencies_parsed_at":"2022-08-07T14:00:24.889Z","dependency_job_id":null,"html_url":"https://github.com/devxoul/Pure","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devxoul%2FPure","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devxoul%2FPure/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devxoul%2FPure/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devxoul%2FPure/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devxoul","download_url":"https://codeload.github.com/devxoul/Pure/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245500427,"owners_count":20625578,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["composition-root","dependency-injection","pure-di","swift"],"created_at":"2024-07-31T15:00:33.549Z","updated_at":"2025-03-25T16:32:36.366Z","avatar_url":"https://github.com/devxoul.png","language":"Swift","funding_links":[],"categories":["Libs","Dependency Injection [🔝](#readme)"],"sub_categories":["Dependency Injection"],"readme":"# Pure\n\n![Swift](https://img.shields.io/badge/Swift-5.0-orange.svg)\n[![CocoaPods](http://img.shields.io/cocoapods/v/Pure.svg)](https://cocoapods.org/pods/Pure)\n[![Build Status](https://github.com/devxoul/Pure/workflows/CI/badge.svg)](https://github.com/devxoul/Pure/actions)\n[![Codecov](https://img.shields.io/codecov/c/github/devxoul/Pure.svg)](https://codecov.io/gh/devxoul/Pure)\n\nPure makes [Pure DI](http://blog.ploeh.dk/2014/06/10/pure-di/) easy in Swift. This repository also introduces a way to do Pure DI in a Swift application.\n\n## Table of Contents\n\n* [Background](#background)\n    * [Pure DI](#pure-di)\n    * [Composition Root](#composition-root)\n        * [AppDependency](#appdependency)\n        * [Testing AppDelegate](#testing-appdelegate)\n        * [Separating AppDelegate](#separating-appdelegate)\n    * [Lazy Dependency](#lazy-dependency)\n        * [Using Factory](#using-factory)\n        * [Using Configurator](#using-configurator)\n    * [Problem](#problem)\n* [Getting Started](#getting-started)\n    * [Depenency and Payload](#dependency-and-payload)\n    * [Module](#module)\n        * [Factory Module](#factory-module)\n        * [Configurator Module](#configurator-module)\n    * [Customizing](#customizing)\n        *  [Storyboard Support](#storyboard-support)\n        *  [URLNavigator Support](#urlnavigator-support)\n* [Installation](#installation)\n* [Contributing](#contribution)\n* [License](#license)\n\n## Background\n\n### Pure DI\n\n[Pure DI](http://blog.ploeh.dk/2014/06/10/pure-di/) is a way to do a dependency injection without a DI container. The term was first introduced by [Mark Seemann](http://blog.ploeh.dk/). The core concept of Pure DI is not to use a DI container and to compose an entire object dependency graph in the [Composition Root](http://blog.ploeh.dk/2011/07/28/CompositionRoot/).\n\n### Composition Root\n\nThe Composion Root is where the entire object graph is resolved. In a Cocoa application, `AppDelegate` is the Composition Root.\n\n#### AppDependency\n\nThe root dependencies are the app delegate's dependency and the root view controller's dependency. The best way to inject those dependencies is to create a struct named `AppDependency` and store both dependencies in it.\n\n```swift\nstruct AppDependency {\n  let networking: Networking\n  let remoteNotificationService: RemoteNotificationService\n}\n\nextension AppDependency {\n  static func resolve() -\u003e AppDependency {\n    let networking = Networking()\n    let remoteNotificationService = RemoteNotificationService()\n\n    return AppDependency(\n      networking: networking\n      remoteNotificationService: remoteNotificationService\n    )\n  }\n}\n```\n\nIt is important to separate a production environment from a testing environment. We have to use an actual object in a production environment and a mock object in a testing environment.\n\nAppDelegate is created automatically by the system using `init()`. In this initializer we're going to initialize the actaul app dependency with `AppDependency.resolve()`. On the other hand, we're going to provide a `init(dependency:)` to inject a mock app dependency in a testing environment.\n\n```swift\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n  private let dependency: AppDependency\n\n  /// Called from the system (it's private: not accessible in the testing environment)\n  private override init() {\n    self.dependency = AppDependency.resolve()\n    super.init()\n  }\n\n  /// Called in a testing environment\n  init(dependency: AppDependency) {\n    self.dependency = dependency\n    super.init()\n  }\n}\n```\n\nThe app dependency can be used as the code below:\n\n```swift\nfunc application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -\u003e Bool {\n  // inject rootViewController's dependency\n  if let viewController = self.window?.rootViewController as? RootViewController {\n    viewController.networking = self.dependency.networking\n  }\n}\n\nfunc application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -\u003e Void) {\n  // delegates remote notification receive event\n  self.dependency.remoteNotificationService.receiveRemoteNotification(userInfo)\n}\n```\n\n#### Testing AppDelegate\n\n`AppDelegate` is one of the most important class in a Cocoa application. It resolves an app dependency and handles app events. It can be easily tested as we separated the app dependency.\n\nThis is an example test case of `AppDelegate`. It verifies that `AppDelegate` correctly injects root view controller's dependency in `application(_:didFinishLaunchingWithOptions)`.\n\n```swift\nclass AppDelegateTests: XCTestCase {\n  func testInjectRootViewControllerDependencies() {\n    // given\n    let networking = MockNetworking()\n    let mockDependency = AppDependency(\n      networking: networking,\n      remoteNotificationService: MockRemoteNotificationService()\n    )\n    let appDelegate = AppDelegate(dependency: mockDependency)\n    appDelegate.window = UIWindow()\n    appDelegate.window?.rootViewController = UIStoryboard(name: \"Main\", bundle: nil).instantiateInitialViewController()\n\n    // when\n    _ = appDelegate.application(.shared, didFinishLaunchingWithOptions: nil)\n\n    // then\n    let rootViewController = appDelegate.window?.rootViewController as? RootViewController\n    XCTAssertTrue(rootViewController?.networking === networking)\n  }\n}\n```\n\nYou can write tests for verifying remote notification events, open url events and even an app termination event.\n\n#### Separating AppDelegate\n\nBut there is a problem: `AppDelegate` is still created by the system while testing. It causes `AppDependency.resolve()` gets called so we have to use a fake app delegate class in a testing environment.\n\nFirst of all, create a new file in the test target. Define a new class named `TestAppDelegate` and implement basic requirements of the delegate protocol.\n\n```swift\n// iOS\nclass TestAppDelegate: NSObject, UIApplicationDelegate {\n  var window: UIWindow?\n}\n\n// macOS\nclass TestAppDelegate: NSObject, NSApplicationDelegate {\n}\n```\n\nThen create another file named **`main.swift`** to your application target. This file will replace the entry point of the application. We are going to provide different app delegates in this file. Don't forget to replace `\"MyAppTests.TestAppDelegate\"` with your project target and class name.\n\n```swift\n// iOS\nUIApplicationMain(\n  CommandLine.argc,\n  CommandLine.unsafeArgv,\n  NSStringFromClass(UIApplication.self),\n  NSStringFromClass(NSClassFromString(\"MyAppTests.TestAppDelegate\") ?? AppDelegate.self)\n)\n\n// macOS\nfunc createAppDelegate() -\u003e NSApplicationDelegate {\n  if let cls = NSClassFromString(\"AllkdicTests.TestAppDelegate\") as? (NSObject \u0026 NSApplicationDelegate).Type {\n    return cls.init()\n  } else {\n    return AppDelegate(dependency: AppDependency.resolve())\n  }\n}\n\nlet application = NSApplication.shared\napplication.delegate = createAppDelegate()\n_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)\n```\n\nFinally, remove the `@UIApplicationMain` and `@NSApplicationMain` from the `AppDelegate`.\n\n```diff\n  // iOS\n- @UIApplicationMain\n  class AppDelegate: UIResponder, UIApplicationDelegate\n\n  // macOS\n- @NSApplicationMain\n  class AppDelegate: NSObject, NSApplicationDelegate\n```\n\nIt is also a good practice to add a test case to verify that the application is using `TestAppDelegate` in a testing environment.\n\n```swift\nXCTAssertTrue(UIApplication.shared.delegate is TestAppDelegate)\n```\n\n### Lazy Dependency\n\n#### Using Factory\n\nIn Cocoa applications, view controllers are created lazily. For example, `DetailViewController` is not created until the user taps an item on `ListViewController`. In this case we have to pass a factory closure of `DetailViewController` to `ListViewController`.\n\n```swift\n/// A root view controller\nclass ListViewController {\n  var detailViewControllerFactory: ((Item) -\u003e DetailViewController)!\n\n  func presentItemDetail(_ selectedItem: Item) {\n    let detailViewController = self.detailViewControllerFactory(selectedItem)\n    self.present(detailViewController, animated: true)\n  }\n}\n\nstatic func resolve() -\u003e AppDependency {\n  let storyboard = UIStoryboard(name: \"Main\", bundle: nil)\n  let networking = Networking()\n\n  let detailViewControllerFactory: (Item) -\u003e DetailViewController = { selectedItem in\n    let detailViewController = storyboard.instantiateViewController(withIdentifier: \"DetailViewController\") as! DetailViewController\n    detailViewController.networking = networking\n    detailViewController.item = selectedItem\n    return detailViewController\n  }\n\n  return AppDependency(\n    networking: networking,\n    detailViewControllerFactory: detailViewControllerFactory\n  )\n}\n```\n\nBut it has a critical problem: we cannot test the factory closure. Because the factory closure is created in the Composition Root but we should not access the Composition Root in a testing environment. What if I forget to inject the `DetailViewController.networking` property?\n\nOne possible approach is to create a factory closure outside of the Composition Root. Note that `Storyboard` and `Networking` is from the Composition Root, and `Item` is from the previous view controller so we have to separate the scope.\n\n```swift\nextension DetailViewController {\n  static let factory: (UIStoryboard, Networking) -\u003e (Item) -\u003e DetailViewController = { storyboard, networking in\n    return { selectedItem in\n      let detailViewController = storyboard.instantiateViewController(withIdentifier: \"DetailViewController\") as! DetailViewController\n      detailViewController.networking = networking\n      detailViewController.item = selectedItem\n      return detailViewController\n    }\n  }\n}\n\nstatic func resolve() -\u003e AppDependency {\n  let storyboard = ...\n  let networking = ...\n  return .init(\n    detailViewControllerFactory: DetailViewController.factory(storyboard, networking)\n  )\n}\n```\n\nNow we can test the `DetailViewController.factory` closure. Every dependencies are resolved in the Composition Root and a selected item can be passed from `ListViewController` to `DetailViewController` in runtime.\n\n#### Using Configurator\n\nThere is another lazy dependency. Cells are created lazily but we cannot use the factory closure because the cells are created by the framework. We can just configure the cells.\n\nImagine that an `UICollectionViewCell` or `UITableViewCell` displays an image. There is an `imageDownloader` which downloads an actual image in a production environment and returns a mock image in a testing environment.\n\n```swift\nclass ItemCell {\n  var imageDownloader: ImageDownloaderType?\n  var imageURL: URL? {\n    didSet {\n      guard let imageDownloader = self.imageDownloader else { return }\n      self.imageView.setImage(with: self.imageURL, using: imageDownloader)\n    }\n  }\n}\n```\n\nThis cell is displayed in `DetailViewController `. `DetailViewController` should inject `imageDownloader` to the cell and sets the `image` property. Like we did in the factory, we can create a configurator closure for it. But this closure takes an existing instance and doens't have a return value.\n\n```swift\nclass ItemCell {\n  static let configurator: (ImageDownloaderType) -\u003e (ItemCell, UIImage) -\u003e Void = { imageDownloader\n    return { cell, image in\n      cell.imageDownloader = imageDownloader\n      cell.image = image\n    }\n  }\n}\n```\n\n`DetailViewController` can have the configurator and use it when configurating cell.\n\n```swift\nclass DetailViewController {\n  var itemCellConfigurator: ((ItemCell, UIImage) -\u003e Void)?\n\n  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -\u003e UICollectionViewCell {\n    ...\n    self.itemCellConfigurator?(cell, image)\n    return cell\n  }\n}\n```\n\n`DetailViewController.itemCellConfigurator` is injected from a factory.\n\n```swift\nextension DetailViewController {\n  static let factory: (UIStoryboard, Networking, (ItemCell, UIImage) -\u003e Void) -\u003e (Item) -\u003e DetailViewController = { storyboard, networking, imageCellConfigurator in\n    return { selectedItem in\n      let detailViewController = storyboard.instantiateViewController(withIdentifier: \"DetailViewController\") as! DetailViewController\n      detailViewController.networking = networking\n      detailViewController.item = selectedItem\n      detailViewController.imageCellConfigurator = imageCellConfigurator\n      return detailViewController\n    }\n  }\n}\n```\n\nAnd the Composition Root finally looks like:\n\n```swift\nstatic func resolve() -\u003e AppDependency {\n  let storybard = ...\n  let networking = ...\n  let imageDownloader = ...\n  let listViewController = ...\n  listViewController.detailViewControllerFactory = DetailViewController.factory(\n    storyboard,\n    networking,\n    ImageCell.configurator(imageDownloader)\n  )\n  ...\n}\n```\n\n### Problem\n\nTheoretically it works. But as you can see in the `DetailViewController.factory` it will be very complicated when there are many dependencies. This is why I created Pure. Pure makes factories and configurators neat.\n\n## Getting Started\n\n### Dependency and Payload\n\nFirst of all, take a look at the factory and the configurator we used in the example code.\n\n```swift\nstatic let factory: (UIStoryboard, Networking, (ItemCell, UIImage) -\u003e Void) -\u003e (Item) -\u003e DetailViewController\nstatic let configurator: (ImageDownloaderType) -\u003e (ItemCell, UIImage) -\u003e Void\n```\n\nThose are the functions that return another function. The outer functions are executed in the Composition Root to inject static dependencies like `Networking` and the inner functions are executed in the view controllers to pass a runtime information like `selectedItem`. The parameter of the outer function is *Dependency*. The parameter of the inner function is *Payload*.\n\n```\nstatic let factory: (UIStoryboard, Networking, (ItemCell, UIImage) -\u003e Void) -\u003e (Item) -\u003e DetailViewController\n                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^      ^^^^\n                                        Dependency                             Payload\n\nstatic let configurator: (ImageDownloaderType) -\u003e (ItemCell, UIImage) -\u003e Void\n                          ^^^^^^^^^^^^^^^^^^^      ^^^^^^^^^^^^^^^^^\n                              Dependency                Payload\n```\n\nPure generalizes the factory and configurator using Dependency and Payload.\n\n### Module\n\nPure treats every class that requires a dependency and a payload as a *Module*. A protocol `Module` requires two types: `Dependency` and `Payload`.\n\n```swift\nprotocol Module {\n  /// A dependency that is resolved in the Composition Root.\n  associatedtype Dependency\n\n  /// A runtime information for configuring the module.\n  associatedtype Payload\n}\n```\n\nThere are two types of module: `FactoryModule` and `ConfiguratorModule`.\n\n#### Factory Module\n\nFactoryModule is a generalized version of factory closure. It requires an initializer which takes both `dependency` and `payload`.\n\n```swift\nprotocol FactoryModule: Module {\n  init(dependency: Dependency, payload: Payload)\n}\n\nclass DetailViewController: FactoryModule {\n  struct Dependency {\n    let storyboard: UIStoryboard\n    let networking: Networking\n  }\n\n  struct Payload {\n    let selectedItem: Item\n  }\n\n  init(dependency: Dependency, payload: Payload) {\n  }\n}\n```\n\nWhen a class conforms to `FactoryModule`, it will have a nested type `Factory`. `Factory.init(dependency:)` takes a dependency of the module and has a method `create(payload:)` which creates a new instance.\n\n```swift\nclass Factory\u003cModule\u003e {\n  let dependency: Module.Dependency\n  func create(payload: Module.Payload) -\u003e Module\n}\n\n// In AppDependency\nlet factory = DetailViewController.Factory(dependency: .init(\n  storyboard: storyboard\n  networking: networking\n))\n\n// In ListViewController\nlet viewController = factory.create(payload: .init(\n  selectedItem: selectedItem\n))\n```\n\n#### Configurator Module\n\nConfiguratorModule is a generalized version of configurator closure. It requires a `configure()` method which takes both `dependency` and `payload`.\n\n```swift\nprotocol ConfiguratorModule: Module {\n  func configure(dependency: Dependency, payload: Payload)\n}\n\nclass ItemCell: ConfiguratorModule {\n  struct Dependency {\n    let imageDownloader: ImageDownloaderType\n  }\n\n  struct Payload {\n    let image: UIImage\n  }\n\n  func configure(dependency: Dependency, payload: Payload) {\n    self.imageDownloader = dependency.imageDownloader\n    self.image = payload.image\n  }\n}\n```\n\nWhen a class conforms to `ConfiguratorModule`, it will have a nested type `Configurator`. `Configurator.init(dependency:)` takes a dependency of the module and has a method `configure(_:payload:)` which configures an existing instance.\n\n```swift\nclass Configurator\u003cModule\u003e {\n  let dependency: Module.Dependency\n  func configure(_ module: Module, payload: Module.Payload)\n}\n\n// In AppDependency\nlet configurator = ItemCell.Configurator(dependency: .init(\n  imageDownloader: imageDownloader\n))\n\n// In DetailViewController\nconfigurator.configure(cell, payload: .init(image: image))\n```\n\nWith `FactoryModule` and `ConfiguratorModule`, the example can be refactored as below:\n\n```swift\nstatic func resolve() -\u003e AppDependency {\n  let storybard = ...\n  let networking = ...\n  let imageDownloader = ...\n  return .init(\n    detailViewControllerFactory: DetailViewController.Factory(dependency: .init(\n      storyboard: storyboard,\n      networking: networking,\n      itemCellConfigurator: ItemCell.Configurator(dependency: .init(\n        imageDownloader: imageDownloader\n      ))\n    ))\n  )\n}\n```\n\n### Customizing\n\n`Factory` and `Configurator` are customizable. This is an example of customized factory:\n\n```swift\nextension Factory where Module == DetailViewController {\n  func create(payload: Module.Payload, extraValue: ExtraValue) -\u003e Payload {\n    let module = self.create(payload: payload)\n    module.extraValue = extraValue\n    return module\n  }\n}\n```\n\n#### Storyboard Support\n\n`FactoryModule` can support Storyboard-instantiated view controllers using customizing feature. The code below is an example for storyboard support of `DetailViewController`:\n\n```swift\nextension Factory where Module == DetailViewController {\n  func create(payload: Module.Payload) -\u003e Payload {\n    let module = self.dependency.storyboard.instantiateViewController(withIdentifier: \"DetailViewController\") as! Module\n    module.networking = dependency.networking\n    module.itemCellConfigurator = dependency.itemCellConfigurator\n    module.selectedItem = payload.selectedItem\n    return module\n  }\n}\n```\n\n#### URLNavigator Support\n\n[URLNavigator](https://github.com/devxoul/URLNavigator) is an elegant library for deeplink support. Pure can be also used in registering a view controller to a navigator.\n\n```swift\nclass UserViewController {\n  struct Payload {\n    let userID: Int\n  }\n}\n\nextension Factory where Module == UserViewController {\n  func create(url: URLConvertible, values: [String: Any], context: Any?) -\u003e Module? {\n    guard let userID = values[\"id\"] else { return nil }\n    return self.create(payload: .init(userID: userID))\n  }\n}\n\nlet navigator = Navigator()\nnavigator.register(\"myapp://user/\u003cid\u003e\", UserViewController.Factory().create)\n```\n\n## Installation\n\n* **Using [CocoaPods](https://cocoapods.org)**:\n\n    ```ruby\n    pod 'Pure'\n    ```\n\n* [Carthage](https://github.com/Carthage/Carthage) is not yet supported.\n\n## Contribution\n\nAny discussions and pull requests are welcomed 💖\n\n* To development:\n\n    ```console\n    $ make project\n    ```\n\n* To test:\n\n    ```console\n    $ swift test\n    ```\n\n## License\n\nPure is under MIT license. See the [LICENSE](LICENSE) file for more info.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevxoul%2FPure","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevxoul%2FPure","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevxoul%2FPure/lists"}