{"id":3219,"url":"https://github.com/prosumma/Guise","last_synced_at":"2025-08-03T13:32:18.610Z","repository":{"id":56913042,"uuid":"53304703","full_name":"Prosumma/Guise","owner":"Prosumma","description":"An elegant, flexible, type-safe dependency resolution framework for Swift","archived":false,"fork":false,"pushed_at":"2023-10-18T21:07:07.000Z","size":445,"stargazers_count":58,"open_issues_count":0,"forks_count":5,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-05-29T13:33:24.959Z","etag":null,"topics":["dependency-injection","dependency-resolution","service-locator","swift","unit-testing"],"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/Prosumma.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2016-03-07T07:27:47.000Z","updated_at":"2024-03-04T02:04:33.000Z","dependencies_parsed_at":"2022-08-20T20:20:47.936Z","dependency_job_id":"26a245a2-1060-4a31-90ba-e83056363e0e","html_url":"https://github.com/Prosumma/Guise","commit_stats":{"total_commits":389,"total_committers":4,"mean_commits":97.25,"dds":0.04884318766066842,"last_synced_commit":"76bb5442c3ffa1cf2992c49aa5c267ad800502d1"},"previous_names":[],"tags_count":35,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Prosumma%2FGuise","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Prosumma%2FGuise/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Prosumma%2FGuise/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Prosumma%2FGuise/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Prosumma","download_url":"https://codeload.github.com/Prosumma/Guise/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228548567,"owners_count":17935221,"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":["dependency-injection","dependency-resolution","service-locator","swift","unit-testing"],"created_at":"2024-01-05T20:16:34.829Z","updated_at":"2025-08-03T13:32:18.580Z","avatar_url":"https://github.com/Prosumma.png","language":"Swift","funding_links":[],"categories":["WebSocket","Dependency Injection"],"sub_categories":["Other free courses","Getting Started","Web View"],"readme":"# Guise 11\n\nGuise is a flexible, minimal dependency resolution framework for Swift.\n\n- [x] Flexible dependency resolution, with optional caching\n- [x] Elegant, straightforward registration\n- [x] Thread-safe\n- [x] Supports `throw`ing, `async` and `MainActor`-isolated initializers and resolution\n- [x] Simplifies unit testing\n- [x] Pass arbitrary state when resolving\n- [x] Lazy resolution\n- [x] Nested containers\n- [x] Swift 6+\n- [x] Support for iOS 13+, macOS 10.15+, watchOS 6+, tvOS 13+\n\n## What's New?\n\nSupport for Swift 6 concurrency, especially `MainActor`-isolated registrations and resolutions in a synchronous `MainActor`-isolated context. Read the section below on concurrency to understand the whys and wherefores of this.\n\n## Basic Documentation\n\n### Registration\n\nTo register a dependency with the container, four facts are needed: The _type_ to be registered, the _tags_ under which it will be registered, the _arguments_ needed in order to resolve the registration, and the _lifetime_ of the registration. The first three uniquely identify a registration. We'll discuss the fourth fact, lifetime, below.\n\nLet's start with these one by one.\n\n#### Types\n\n```swift\nlet container = Container()\ncontainer.register(Service.self) { _ in\n  Service()\n}\n```\n\nThe first argument to `register` tells Guise (and Swift) which type we're registering, and the block passed at the end tells Guise how to construct that type. (Ignore the `_` parameter for the moment.)\n\nSince the type we're registering is the same as the return type of the block, we can omit the first argument of `register`:\n\n```swift\ncontainer.register { _ in Service() }\n```\n\nOne of the main purposes of dependency injection is to locate implementations of abstract interfaces, so that they can be substituted in unit tests or for some other purpose. In fact, that's why this framework is called Guise.\n\n\u003e **Guise**, _n._ An external form, appearance, or manner of presentation, typically concealing the true nature of something.\n\n```swift\nprotocol Service {\n  func performService() async\n}\n\nclass ConcreteService: Service {\n  // Perhaps this one talks to a real database\n}\n\nclass TestService: Service {\n  // This is the one we want in unit tests\n}\n\nlet container = Container()\ncontainer.register(Service.self) { _ in\n  ConcreteService()\n}\n```\n\nNotice that in this example, the block returns `ConcreteService` but the registered type is `Service`. Another way to express this is\n\n```swift\ncontainer.register { _ in ConcreteService() as Service }\n```\n\nLater, when we resolve this registration, we ask for `Service`, not `ConcreteService`.\n\nFor simple registrations, Guise has a convenient overload which uses an `@autoclosure`:\n\n```swift\ncontainer.register(instance: ConcreteService() as Service)\n```\n\n#### Tags\n\nTags can help locate, describe, and disambiguate registrations. A tag can be any type which is both `Hashable` and `Sendable`, such as `String`, `UUID`, `Int`\nor a custom type you create yourself.\n\n```swift\nenum Types: Hashable, Sendable {\n  case plugin\n}\n\ncontainer.register(Plugin.self, tags: Types.plugin, 1, instance: PluginImpl1())\n```\n\nThe order of tags is not important and any number may be used. Tags are collected into a `Set\u003cAnySendableHashable\u003e`, so repetition has no effect.\n\nBecause the tags used in a registration are part of the unique `Key` that identifies that registration, the same tags must be used when resolving:\n\n```swift\nlet plugin: Plugin = try container.resolve(Plugin.self, tags: 1, Types.plugin) \n```\n\nThe tags weren't given in the same order as when the registration was made, but that makes no difference.\n\nTags become really powerful when resolving arrays of dependencies. In that case, only a subset of the tags needs to be specified. See the discussion on arrays in the resolution section of this README.\n\n#### Arguments\n\nIt's very common to need to pass some state to a dependency when resolving. Guise supports any number of arguments.\n\n```swift\nclass Service {\n  let id: Int\n  let state: String\n}\n\ncontainer.register { _, id, state in\n  Service(id: id, state: state)\n}\n```\n\nWhen resolving, these arguments must be given or Guise will throw an error:\n\n```swift\nlet service: Service = try container.resolve(args: 1, \"foo\")\n```\n\n#### Lifetimes\n\nGuise supports two lifetimes: transient and singleton. Transient is the default.\n\nIn a transient registration, a new instance of the dependency is created and returned each time. In other words, the resolution factory that is passed as the last argument to `register` is called, the arguments are passed, and its result is returned to the caller.\n\nIn a singleton registration, the factory is invoked the first time, but every subsequent request for that registration returns the same instance.\n\n```swift\ncontainer.register(lifetime: .singleton, instance: Service())\n```\n\n#### Transitive dependencies\n\nOne of the primary functions of dependency injection is to locate and resolve complex hierarchies of dependencies. Guise can do this as well. The first argument of every resolution block is an instance of `Resolver`, which allows registrations to be located and resolved.\n\n```swift\nclass Database {}\nclass Service {\n  let database: Database\n  init(database: Database) {\n    self.database = database\n  }\n}\n\ncontainer.register(lifetime: .singleton, instance: Database())\ncontainer.register(lifetime: .singleton) { r in\n  Service(database: try r.resolve())\n}\n```\n\nWhenever we resolve `Service`, the `Database` parameter in its constructor will be located and resolved.\n\nThis pattern is so common that Guise has a higher-order function, `auto`, that can handle any number of dependencies:\n\n```swift\ncontainer.register(lifetime: .singleton, factory: auto(Service.init))\n```\n\nIn order to use `auto`, the sub-dependencies must not have any tags or factory arguments.\n\n### Resolution\n\nResolution looks up and instantiates a dependency given its type, tags, and factory arguments, and taking into account its lifetime.\n\nResolution is simpler than registration, so a few examples will suffice:\n\n```swift\nclass Service {}\ncontainer.register(instance: Service())\n\n// Two alternate ways to resolve the above registration\nlet service: Service = try container.resolve()\nlet service = try container.resolve(Service.self)\n\ncontainer.register(tags: 2, instance: Service())\nlet service: Service = try container.resolve(tags: 2)\n\nclass Something {\n  let id: Int\n\n  init(id: Int) { self.id = id }\n}\n\ncontainer.register { _, id in\n  Something(id: id)\n}\nlet something = try container.resolve(Something.self, args: 7)\n```\n\n#### Optional Resolution\n\nGuise can resolve optionals as the wrapped type:\n\n```swift\nclass Service {}\ncontainer.register(instance: Service())\nlet service: Service? = try container.resolve()\n```\n\nWhat happens behind the scenes is that Guise first looks for the exact registration, i.e., a registration of the type `Service?`. If it doesn't find that, then it attempts to resolve `Service`.\n\nWhen resolving an optional, Guise returns `nil` instead of throwing an error if the registration cannot be found.\n\n#### Array Resolution\n\nImagine a plugin architecture in which we want to locate and resolve many instances of the same type.\n\n```swift\nprotocol Plugin {}\n\ncontainer.register(Plugin.self, tags: UUID(), instance: Plugin1())\ncontainer.register(Plugin.self, tags: UUID(), instance: Plugin2())\ncontainer.register(Plugin.self, tags: UUID(), instance: Plugin3())\n```\n\nWe can get all of these plugins very easily:\n\n```swift\nlet plugins: [Plugin] = try container.resolve()\n```\n\nJust as with optional resolution, Guise first looks for an exact match for this registration. Not finding one, it notices that this is trying to resolve an array. It then locates all registrations of type `Plugin` and attempts to resolve them all. If any fail, all fail.\n\nWhen resolving an array, tags are processed differently. Guise looks for all registrations containing all of the given tags.\n\n```swift\ncontainer.register(Plugin.self, tags: \"type1\", UUID(), instance: Plugin1())\ncontainer.register(Plugin.self, tags: \"type1\", UUID(), instance: Plugin2())\ncontainer.register(Plugin.self, tags: \"type2\", UUID(), instance: Plugin3())\ncontainer.register(Plugin.self, tags: \"type2\", UUID(), instance: Plugin4())\n```\n\nHere we have four plugins registered. Each is disambiguated with an anonymous `UUID` and divided into two types: type 1 and type 2. To get all of the type 1 registrations…\n\n```swift\nlet plugins: [Plugin] = try container.resolve(tags: \"type1\")\n```\n\nThis gets `Plugin1` and `Plugin2` but not `Plugin3` and `Plugin4`. Of course, we can still get all four of them with this incantation:\n\n```swift\nlet plugins: [Plugin] = try container.resolve()\n```\n\nIf no registrations are found, Guise returns an empty array by default instead of throwing an error.\n\n#### Lazy Resolution\n\nOccasionally there's a need to depend on a service that isn't ready yet. Or we wish to prevent a cycle because two services depend on each other.\n\nOne way to solve this problem is to pass an instance of the `Resolver` itself:\n\n```swift\nclass Service {\n  weak var resolver: Resolver! \n\n  init(resolver: Resolver) {\n    self.resolver = resolver\n  }\n\n  func performService() throws {\n    let database = try resolver.resolve(Database.self)\n    database.doSomething()\n  }\n}\n```\n\nThe problem with the pattern above is that it breaks one of the fundamental rules of dependency injection: make dependencies explicit. A user of `Service` must read the source code in order to know what other dependencies it has. This makes the class harder to use and harder to test.\n\nGuise solves this problem with lazy resolvers.\n\n```swift\nclass Service {}\ncontainer.register(tags: \"s\", instance: Service())\n\nlet lr: LazyResolver\u003cService\u003e = try container.resolve()\n```\n\nLazy resolvers don't have to be registered. Guise automatically constructs them as needed. A lazy resolver resolves dependencies of the type `Service`. Tags and arguments are specified when resolving:\n\n```swift\nlet service = lr.resolve(tags: \"s\")\n```\n\n### Async\n\nGuise supports `async` registrations and resolution.\n\n```swift\nclass Service {\n  let database: Database\n\n  init(database: Database) async {\n    self.database = database\n    await database.setup()\n  }\n}\n\ncontainer.register { r in\n  try await Service(database: r.resolve())\n}\nlet service = try await container.resolve(Service.self)\n```\n\nAny synchronous registration may be resolved asynchronously, but the reverse is not true. By default, if an attempt is made to resolve an `async` registration in a synchronous context, Guise throws `.requiresAsync`.\n\n### Concurrency\n\nGuise has special support for `MainActor`-isolated registrations and resolutions. This uses the `isolation` parameter to distinguish between other overloads of `register` and `resolve`:\n\n```swift\nregistrar.register(isolation: MainActor.shared) { _ in\n  MyViewController()\n}\nlet myViewController = try resolver.resolve(MyViewController.self, isolation: MainActor.shared)\n```\n\nThe obvious question is: Why only support `MainActor`? Why not support any global actor? Well, actually Guise _does_ support any global actor, it's just that both registration and resolution must be `async`:\n\n```swift\nregistrar.register { @MyActor _ async in\n  MyService()\n}\nlet myService: MyService = try await resolver.resolve()\n```\n\nThe special support for `MainActor` is because, in general, we want `MainActor`-isolated resolutions _not_ to be `async` within the `MainActor` context, such as a view controller:\n\n```swift\noverride func awakeFromNib() {\n  super.awakeFromNib()\n  MainActor.assumeIsolated {\n    // Note that this is synchronous.\n    let otherViewController = try resolver.resolve(OtherViewController.self, isolation: MainActor.shared)\n  }\n}\n```\n\nCurrently, Apple does not provide a mechanism to \"flow\" the isolation context all the way through the internal dance of a DI framework, including type erasure, etc. while maintaining synchronous call semantics. I'm aware of `isolated` parameters and they looked promising. You _can_ flow the actor all the way through using that, but the problem is that when you try to resolve, everything is `async`, which kills ergonomics, particularly for `MainActor`. To call an actor-isolated method without using `await`, the Swift compiler must be able to prove statically _at compile time_ that the actor-isolated method is being called within\nthe very same isolation context.\n\nI made the pragmatic choice to make `MainActor` a special case. And it really should be, because its semantics are a bit different from other actors. It's the actor within which all UI code executes, including a mountain of synchronous legacy code. As a result, we want resolution of `MainActor`-isolated types to be synchronous when possible. This greatly improves ergonomics.\n\nAs a matter of fact, you can resolve `MainActor`-isolated registrations in an `async` context if you wish. For example:\n\n```swift\nregistrar.register(isolation: MainActor.shared) { _ in\n  MyViewController()\n}\nlet myViewController: MyViewController = try await resolver.resolve()\n```\n\n#### Why the `isolation` parameter instead of using a different name, like `registerMain`?\n\nThere are two imperfect reasons for this:\n\n1. I wanted to stay consistent with using overloads of `register` and `resolve` for everything, whether synchronous, asynchronous, or `MainActor`-isolated.\n2. If Swift ever does make it possible to flow the actor context through and preserve synchronous call semantics, this is the obvious syntax to use, so it's a bit of future-proofing.\n\nIf you think `register(isolation: MainActor.shared)` is long-winded, it's easy enough to create an extension method:\n\n```swift\npublic extension Registrar {\n  func registerMain\u003cT, each Tag: Hashable \u0026 Sendable, each Arg: Sendable\u003e(\n    _ type: T.Type = T.self,\n    tags: repeat each Tag,\n    lifetime: Lifetime = .transient,\n    factory: @escaping @MainActor @Sendable (any Resolver, repeat each Arg) throws -\u003e T\n  ) -\u003e Key\u003cT\u003e {\n    register(type, isolation: MainActor.shared, tags: repeat each tags, lifetime: lifetime, factory: factory)\n  }\n}\n```\n\n#### `mainauto`\n\nAnalogous to `auto`, Guise has a helper function called `mainauto` which can be used to simplify registration of `MainActor`-isolated types with constructor dependencies:\n\n```swift\nclass MyViewController: UIViewController {\n  let myService: MyService\n  init(myService: MyService) {\n    self.myService = myService\n  }\n}\nawait MainActor.run {\n  registrar.register(isolation: MainActor.shared, factory: mainauto(MyViewController.init))\n}\n```\n\nWhy is `MainActor.run` necessary here? It really shouldn't be. Unfortunately, there's a bug in the Swift compiler's static concurrency analysis. It thinks that `MyViewController.init` \u0026mdash; which is a `MainActor`-isolated initializer \u0026mdash; is being called here instead of just passed as a higher-order function parameter. So unfortunately we must use `MainActor.run`. Hopefully this bug will be fixed in an upcoming version of Swift. If you don't use `mainauto`, you don't have to do this:\n\n```swift\nclass MyViewController: UIViewController {\n  let myService: MyService\n  init(myService: MyService) {\n    self.myService = myService\n  }\n}\nregistrar.register(isolation: MainActor.shared) { r in\n  try MyViewController(myService: r.resolve())\n}\n```\n\n#### The Resolution Matrix\n\nDependencies often depend on other dependencies. These dependencies may have differences in isolation, which can cause problems when resolving. For example:\n\n```swift\n// This type is MainActor-isolated\nclass ViewController: UIViewController {}\nregistrar.register(\n  isolation: MainActor.shared,\n  instance: ViewController()\n)\n\n// This type is non-isolated, which we call \"sync\" in Guise.\nclass Presenter {\n  let vc: ViewController\n  init(vc: ViewController) {\n    self.vc = vc\n  }\n  @MainActor\n  func present(on other: UIViewController) {\n    other.present(vc, animated: true)\n  }\n}\n// This won't even compile\nregistrar.register(factory: auto(Presenter.init))\n```\n\nThe reason `Presenter`'s registration won't compile is because its dependency, `ViewController`, has a MainActor-isolated initializer, which must\nbe called within a `MainActor`-isolated context. There are several ways we can solve this.\n\nFirst, we can use async registration:\n\n```swift\nregistrar.register { r in\n  try await Presenter(vc: r.resolve())\n}\n```\n\nThis works but kills ergonomics in many cases because now we must also resolve using `await`:\n\n```swift\nlet presenter: Presenter = try await resolver.resolve()\n```\n\nThe second solution is to make `Presenter`'s registration `MainActor`-isolated as well:\n\n```swift\nregistrar.register(isolation: MainActor.shared, mainauto(Presenter.init))\n```\n\nBut now we must always resolve in a `MainActor`-isolated context.\n\nThe best solution for this, in my opinion, is to use a `LazyResolver`. For that, we have to rewrite our `Presenter` a bit:\n\n```swift\nclass Presenter {\n  let lvc: LazyViewController\n  init(lvc: LazyResolver\u003cViewController\u003e) {\n    self.lvc = lvc\n  }\n  @MainActor\n  func present(on other: UIViewController) throws {\n    let vc = try lvc.resolve(isolation: MainActor.shared)\n    other.present(vc, animated: true)\n  }\n}\n```\n\nSo what's the matrix? The matrix tells us what kind of resolution can resolve what kind of registration.\n\n| RSLV :point_down: RGSTR :point_right: | Sync               | Async              | MainActor          |\n|:--------------------------------------|--------------------|--------------------|--------------------|\n| **Sync**                              | :white_check_mark: | :x:                | :x:                |  \n| **Async**                             | :white_check_mark: | :white_check_mark: | :white_check_mark: |\n| **MainActor**                         | :white_check_mark: | :x:                | :white_check_mark: |\n\nAcross the top are the different kinds of registrations and along the side are the different kinds of resolutions. This tells us, for example, that a synchronous non-isolated (\"sync\") registration can be resolved by any kind of resolution. Asynchronous non-isolated (\"async\") resolution can resolve any kind of registration. And so on.\n\nMy recommendation is to use a `LazyResolver` everywhere you see an :x: in the table above.\n\n### Assemblies\n\nIn a complex application with many modules, it can be helpful to organization registrations exported from the module. Guise provides _assemblies_ for this purpose.\n\n```swift\nclass AwesomeAssembly: Assembly {\n  func register(in registrar: any Registrar) {\n    registrar.register(assemblies: CoolAssembly())\n    registrar.register(lifetime: .singleton, instance: Service())\n  }\n\n  // This method is optional. A default implementation\n  // is provided, which does nothing.\n  func registered(to resolver: any Resolver) {\n    do {\n      let service = try resolver.resolve(Service.self)\n      service.configure()\n    } catch {\n      // Handle error\n    }\n  }\n}\n```\n\nAssemblies are organized in a hierarchy. An assembly should register its dependent assemblies using `register(assemblies:)`.\n\nAssemblies are keyed by their type, so adding the same assembly twice will not result in double registration.\n\nThe act of assembling first creates an ordered set of assemblies, i.e., a list without duplicates in order of first registration. It then iterates through this list and calls `register(in:)` on each one. After which it iterates through the list and calls `registered(to:)` on each one.\n\nThe purpose of `registered(to:)` is to perform additional initialization after dependencies have been registered without exposing the dependencies outside of the assembly.\n\nOnce all dependencies have been registered, call `assemble` on the container to make everything work. Additional assemblies can be passed as arguments to `assemble` or it can be called without arguments if they've already been registered.\n\n```swift\ncontainer.assemble(AwesomeAssembly(), CoolAssembly())\n```\n\nor\n\n```swift\ncontainer.register(assemblies: AwesomeAssembly(), CoolAssembly())\ncontainer.assemble()\n```\n\n### Nested Containers\n\nGuise supports nested `Container`s. When constructing a `Container`, simply pass its parent in the constructor:\n\n```swift\nlet parent = Container()\nlet child = Container(parent: parent)\n```\n\nWhen resolving, if an entry can't be found in the child, the parent is searched. Child entries always override parent entries for matching `Key`s. The reverse is **not** true: A parent has no knowledge of its child containers. Searching the parent directly will not discover any child entries.\n\n### Nested Containers \u0026amp; Assemblies\n\nAssemblies are simply a way to make many registrations _en masse_. If a container cannot find a registration, it will search its parent. This is not true of assemblies. Assemblies are specific to a container.\n\n```swift\nlet parent = Container()\nparent.assemble(CoolAssembly())\nlet child = Container(parent: parent)\nchild.assemble(AwesomeAssembly())\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprosumma%2FGuise","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fprosumma%2FGuise","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprosumma%2FGuise/lists"}