├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Images └── header.png ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Container │ ├── Container.swift │ ├── Dependency.swift │ ├── DependencyContainer.swift │ ├── DependencyLabel.swift │ ├── DependencyScope.swift │ ├── Factory.swift │ ├── FactoryStorage.swift │ ├── Protocols.swift │ ├── RecursiveLock.swift │ ├── StoreKey.swift │ ├── StrongBox.swift │ ├── WeakBox.swift │ └── WeakContainer.swift └── Tests └── ContainerTests └── ContainerTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Images/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartleby/Container/b64c2916e3ec9d0f12d3de0a922fe570031f365c/Images/header.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alex Artemev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Container", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "Container", 12 | targets: ["Container"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "Container", 23 | dependencies: []), 24 | .testTarget( 25 | name: "ContainerTests", 26 | dependencies: ["Container"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | Container 6 | ======== 7 | 8 | Container is a lightweight [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) framework for Swift. 9 | 10 | 11 | ## Installation 12 | 13 | is available in the [Swift Package Manager](https://swift.org/package-manager/). 14 | 15 | ### Swift Package Manager 16 | 17 | ``` 18 | https://github.com/bartleby/Container.git 19 | ``` 20 | 21 | 22 | ## Basic Usage 23 | 24 | Firstly, you must defined your containers 25 | 26 | 27 | ```swift 28 | extension Container { 29 | static let services = Container(ContainerConfigurator.services) 30 | static let assemblies = Container(ContainerConfigurator.assemblies) 31 | static let appearance = Container(ContainerConfigurator.appearance) 32 | } 33 | 34 | struct ContainerConfigurator { 35 | static var services: DependencyResolver { 36 | let container = DependencyContainer() 37 | 38 | container.apply(Service() as ServiceProtocol) 39 | container.apply(ServiceTwo(), label: .two) 40 | 41 | return container 42 | } 43 | 44 | static var assemblies: DependencyResolver { 45 | let container = DependencyContainer() 46 | 47 | container.apply(MainAssembly()) 48 | container.apply(AuthorizationAssembly()) 49 | container.apply(RegistrationAssembly()) 50 | container.apply(OnboardingAssembly()) 51 | container.apply(SettingsAssembly()) 52 | 53 | return container 54 | } 55 | 56 | static var appearance: DependencyResolver { 57 | let container = DependencyContainer() 58 | 59 | container.apply(Color.red, label: .redColor) 60 | 61 | return container 62 | } 63 | } 64 | 65 | ``` 66 | 67 | Then get an instance of a service from the container. 68 | 69 | ```swift 70 | let resolver = Container.services.resolver 71 | let service = resolver.resolve(Service.self, scope: .unowned) 72 | service.run() 73 | ``` 74 | 75 | You can also use Propertywrapers 76 | 77 | ```swift 78 | class ViewModel { 79 | 80 | @Dependency(.services, scope: .strong) var config: AppConfigServiceProtocol 81 | 82 | //... 83 | 84 | func setup() { 85 | let token = config.obtain(for: .token) 86 | //... 87 | } 88 | 89 | } 90 | ``` 91 | 92 | ### Scope 93 | 94 | You can specify the `scope` for your dependencies, `.weak` `.strong` and `.unowned` 95 | 96 | `.weak` scope is used by default 97 | 98 | ```swift 99 | @Dependency(.services) var service: ServiceProtocol // .weak scope is used by default 100 | ``` 101 | 102 | How it's work 103 | 104 | - `weak` Keeps the object in memory while at least one object refers to it 105 | 106 | 107 | - `strong` An analogue of Singletone, after creating the object, it always remains in memory even if no one refers to it 108 | 109 | 110 | - `unowned` The object is not stored in memory and each time create a new object 111 | 112 | 113 | ### Labels 114 | 115 | For the same types, you can specify Label 116 | For exemple: 117 | 118 | ```swift 119 | 120 | container.apply(Color.red, label: .redColor) 121 | container.apply(Color.green, label: .greenColor) 122 | container.apply(Color.orange, label: .orangeColor) 123 | 124 | ``` 125 | 126 | In order to create your own `Label`, you need to extension the structure of `DependencyLabel` 127 | 128 | ```swift 129 | extension DependencyLabel { 130 | static let redColor = ContainerLabel(value: "redColor") 131 | static let greenColor = ContainerLabel(value: "greenColor") 132 | static let orangeColor = ContainerLabel(value: "orangeColor") 133 | } 134 | ``` 135 | 136 | Then you can specify a label when using 137 | 138 | ```swift 139 | @Dependency(.appearance, scope: .strong, label: .redColor) var redColor: Color 140 | ``` 141 | 142 | 143 | 144 | ## Assembly module 145 | 146 | You'r also can use `Assembly` for assembly your Modules with `Container` 147 | 148 | First, register a `Assembly` to a `Container` 149 | 150 | ```swift 151 | 152 | extension Container { 153 | static let assembly = Container(ContainerConfigurator.assemblies) 154 | //... 155 | } 156 | 157 | struct ContainerConfigurator { 158 | 159 | static var assemblies: DependencyResolver { 160 | let container = DependencyContainer() 161 | 162 | container.apply(MainAssembly()) 163 | container.apply(AuthorizationAssembly()) 164 | container.apply(RegistrationAssembly()) 165 | container.apply(OnboardingAssembly()) 166 | container.apply(SettingsAssembly()) 167 | 168 | return container 169 | } 170 | 171 | //... 172 | } 173 | ``` 174 | 175 | Then implement the MainAssembly class 176 | 177 | 178 | ```swift 179 | protocol Assembly { 180 | associatedtype C: CoordinatorType 181 | associatedtype M 182 | 183 | func build(coordinator: C) -> M 184 | } 185 | 186 | typealias MainModule = Module 187 | 188 | struct MainAssembly: Assembly { 189 | @Dependency(.services, scope: .strong) var config: AppConfigServiceProtocol 190 | 191 | func build(coordinator: MainCoordinator) -> MainModule { 192 | 193 | // View 194 | let view = MainViewController() 195 | 196 | // Router 197 | let router = MainRouter(coordinator: coordinator) 198 | 199 | // ViewModel 200 | let viewModel = MainViewModel(router: router, config: config) 201 | 202 | // Configure 203 | viewModel.view = view 204 | view.output = viewModel 205 | 206 | return Module(view: view, input: viewModel, output: viewModel) 207 | } 208 | } 209 | ``` 210 | 211 | ### How to usage 212 | 213 | ```swift 214 | class MainCoordinator: BaseCoordinator { 215 | 216 | // MARK: - Dependencies 217 | @Dependency(.assemblies) var mainAssembly: MainAssembly 218 | 219 | override func start() { 220 | let module = mainAssembly.build(coordinator: self) 221 | router.setRootModule(module) 222 | } 223 | } 224 | 225 | ``` 226 | 227 | ## Example Apps 228 | 229 | Coming soon 230 | 231 | 232 | ## License 233 | 234 | MIT license. See the [LICENSE file](LICENSE) for details. 235 | -------------------------------------------------------------------------------- /Sources/Container/Container.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 iDevs.io. All rights reserved. 2 | 3 | import Foundation 4 | 5 | public struct Container { 6 | public var resolver: DependencyResolver 7 | 8 | public init(_ resolver: DependencyResolver) { 9 | self.resolver = resolver 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Container/Dependency.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 iDevs.io. All rights reserved. 2 | 3 | import Foundation 4 | 5 | @propertyWrapper 6 | final public class Dependency { 7 | 8 | // MARK: - Properties 9 | private let label: DependencyLabel 10 | private let scope: DependencyScope 11 | private var container: Container 12 | public var wrappedValue: T { value } 13 | 14 | // MARK: - Lazy Properties 15 | private lazy var value: T = { 16 | container.resolver.resolve(T.self, scope: scope, label: label) 17 | }() 18 | 19 | // MARK: - Init/Deinit 20 | public init( 21 | _ container: Container, 22 | scope: DependencyScope = .weak, 23 | label: DependencyLabel = .none 24 | ) { 25 | self.container = container 26 | self.label = label 27 | self.scope = scope 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Container/DependencyContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 iDevs.io. All rights reserved. 2 | 3 | import Foundation 4 | 5 | final public class DependencyContainer { 6 | 7 | // MARK: - Properties 8 | public var weakBoxHolder = [String : WeakContainer]() 9 | public var strongBoxHolder = [String : AnyObject]() 10 | public let factoryStorage: FactoryStorageProtocol = FactoryStorage() 11 | private var lock: RecursiveLock = RecursiveLock() 12 | 13 | // MARK: - Init/Deinit 14 | public init() {} 15 | } 16 | 17 | extension DependencyContainer: DependencyApplier { 18 | public func apply(_ builder: @escaping @autoclosure () -> T, label: DependencyLabel) { 19 | lock.sync { 20 | let factory = Factory(builder: builder) 21 | factoryStorage.apply(factory, label: label) 22 | } 23 | } 24 | 25 | public func apply(_ builder: @autoclosure @escaping () -> T) { 26 | lock.sync { 27 | let factory = Factory(builder: builder) 28 | factoryStorage.apply(factory, label: .none) 29 | } 30 | } 31 | } 32 | 33 | extension DependencyContainer: DependencyResolver { 34 | public func resolve(_ type: T.Type, scope: DependencyScope = .weak, label: DependencyLabel) -> T { 35 | lock.sync { 36 | let factory = self.factoryStorage.resolve(type, label: label) 37 | return scope.resolve(resolver: self, factory: factory, label: label) 38 | } 39 | } 40 | 41 | public func resolve(_ type: T.Type, scope: DependencyScope = .weak) -> T { 42 | resolve(type, scope: scope, label: .none) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Container/DependencyLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartleby on 23.08.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct DependencyLabel { 11 | 12 | // MARK: - Properties 13 | let value: String 14 | 15 | // MARK: - Init/Deinit 16 | public init(value: String) { 17 | self.value = value 18 | } 19 | } 20 | 21 | // MARK: - Equatable 22 | extension DependencyLabel: Equatable {} 23 | 24 | public extension DependencyLabel { 25 | static let none = DependencyLabel(value: "none") 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Container/DependencyScope.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 iDevs.io. All rights reserved. 2 | 3 | import Foundation 4 | 5 | public enum DependencyScope { 6 | case weak 7 | case unowned 8 | case strong 9 | 10 | func resolve(resolver: DependencyResolver, factory: Factory, label: DependencyLabel) -> T { 11 | switch self { 12 | case .weak: 13 | return resolver.weakBox(label: label) { factory.builder() } 14 | case .strong: 15 | return resolver.strongBox(label: label) { factory.builder() } 16 | case .unowned: 17 | return factory.builder() 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Container/Factory.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 iDevs.io. All rights reserved. 2 | 3 | import Foundation 4 | 5 | public struct Factory { 6 | 7 | // MARK: - Properties 8 | var builder: () -> T 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Container/FactoryStorage.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 iDevs.io. All rights reserved. 2 | 3 | import Foundation 4 | 5 | final internal class FactoryStorage: FactoryStorageProtocol { 6 | 7 | // MARK: - Typealias 8 | typealias FactoryCollection = [String : Any] 9 | 10 | // MARK: - Private Properties 11 | private var factoryCollection = FactoryCollection() 12 | 13 | // MARK: - Public methods 14 | func apply(_ factory: Factory, label: DependencyLabel) { 15 | let key = StoreKey(T.self, label: label).key 16 | factoryCollection[key] = factory 17 | } 18 | 19 | func resolve(_ type: T.Type, label: DependencyLabel) -> Factory { 20 | let key = StoreKey(type, label: label).key 21 | guard let factory = factoryCollection[key] else { 22 | fatalError("Factory '\(String(describing: type))' has't been registered, use 'apply( _:)' method") 23 | } 24 | return factory as! Factory 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Container/Protocols.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 iDevs.io. All rights reserved. 2 | 3 | import Foundation 4 | 5 | public protocol DependencyApplier { 6 | func apply(_ builder: @escaping @autoclosure () -> T, label: DependencyLabel) 7 | func apply(_ builder: @escaping @autoclosure () -> T) 8 | } 9 | 10 | public protocol DependencyResolver: WeakBox, StrongBox { 11 | func resolve(_ type: T.Type, scope: DependencyScope, label: DependencyLabel) -> T 12 | func resolve(_ type: T.Type, scope: DependencyScope) -> T 13 | } 14 | 15 | public protocol FactoryStorageProtocol: AnyObject { 16 | func apply(_ factory: Factory, label: DependencyLabel) 17 | func resolve(_ type: T.Type, label: DependencyLabel) -> Factory 18 | } 19 | 20 | public protocol StrongBox: AnyObject { 21 | var strongBoxHolder: [String : AnyObject] { set get } 22 | } 23 | 24 | public protocol WeakBox: AnyObject { 25 | var weakBoxHolder: [String : WeakContainer] { set get } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Container/RecursiveLock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartleby on 23.08.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | final class RecursiveLock { 11 | 12 | // MARK: - Private methods 13 | private let lock = NSRecursiveLock() 14 | 15 | // MARK: - Public methods 16 | func sync(action: () -> T) -> T { 17 | lock.lock() 18 | defer { lock.unlock() } 19 | return action() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Container/StoreKey.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 iDevs.io. All rights reserved. 2 | 3 | import Foundation 4 | 5 | internal struct StoreKey { 6 | 7 | // MARK: - Properties 8 | fileprivate let objectType: Any.Type 9 | fileprivate let label: DependencyLabel 10 | var key: String { String(hashValue) } 11 | 12 | // MARK: - Init/Deinit 13 | internal init(_ objectType: Any.Type, label: DependencyLabel = .none) { 14 | self.objectType = objectType 15 | self.label = label 16 | } 17 | } 18 | 19 | // MARK: Hashable 20 | extension StoreKey: Hashable { 21 | func hash(into hasher: inout Hasher) { 22 | hasher.combine(String(describing: objectType).hashValue ^ label.value.hashValue) 23 | } 24 | } 25 | 26 | // MARK: Equatable 27 | extension StoreKey: Equatable { 28 | static func == (lhs: StoreKey, rhs: StoreKey) -> Bool { 29 | return lhs.objectType == rhs.objectType && lhs.label == rhs.label 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Container/StrongBox.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 iDevs.io. All rights reserved. 2 | 3 | import Foundation 4 | 5 | extension StrongBox { 6 | func strongBox(label: DependencyLabel, configure: () -> T) -> T { 7 | let key = StoreKey(T.self, label: label).key 8 | if let object = self.strongBoxHolder[key] { 9 | return object as! T 10 | } 11 | let object = configure() 12 | strongBoxHolder[key] = object as AnyObject 13 | return object 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Container/WeakBox.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 iDevs.io. All rights reserved. 2 | 3 | import Foundation 4 | 5 | extension WeakBox { 6 | func weakBox(label: DependencyLabel, configure: () -> T) -> T { 7 | let key = StoreKey(T.self, label: label).key 8 | if let object = self.weakBoxHolder[key]?.value as? T { 9 | return object 10 | } 11 | let object = configure() 12 | weakBoxHolder[key] = WeakContainer(value: object as AnyObject) 13 | return object 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Container/WeakContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 iDevs.io. All rights reserved. 2 | 3 | import Foundation 4 | 5 | public final class WeakContainer where T: AnyObject { 6 | 7 | // MARK: - Properties 8 | weak var value: T? 9 | 10 | // MARK: - Init/Deinit 11 | init(value: T) { 12 | self.value = value 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/ContainerTests/ContainerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Container 3 | 4 | class TestClass { 5 | let id: String = UUID().uuidString 6 | } 7 | 8 | extension DependencyLabel { 9 | static let test: DependencyLabel = DependencyLabel(value: "test") 10 | } 11 | 12 | extension Container { 13 | static let services = Container(ContainerConfigurator.services) 14 | } 15 | 16 | struct ContainerConfigurator { 17 | static var services: DependencyResolver { 18 | let container = DependencyContainer() 19 | 20 | // Setup Services 21 | container.apply(TestClass()) 22 | container.apply(TestClass(), label: .test) 23 | 24 | return container 25 | } 26 | } 27 | 28 | 29 | final class ContainerTests: XCTestCase { 30 | 31 | func testWeakContainer() throws { 32 | let container = Container.services 33 | 34 | var class1: TestClass? = container.resolver.resolve(TestClass.self, scope: .weak) 35 | var class2: TestClass? = container.resolver.resolve(TestClass.self, scope: .weak) 36 | 37 | XCTAssertTrue(class1?.id == class2?.id) 38 | 39 | class1 = nil 40 | 41 | class1 = container.resolver.resolve(TestClass.self, scope: .weak) 42 | 43 | XCTAssertTrue(class1?.id == class2?.id) 44 | 45 | let oldId = class1?.id 46 | 47 | class1 = nil 48 | class2 = nil 49 | 50 | class1 = container.resolver.resolve(TestClass.self, scope: .weak) 51 | class2 = container.resolver.resolve(TestClass.self, scope: .weak) 52 | 53 | XCTAssertFalse(class1?.id == oldId) 54 | XCTAssertFalse(class2?.id == oldId) 55 | 56 | } 57 | 58 | func testUnownedContainer() throws { 59 | let container = Container.services 60 | 61 | let class1 = container.resolver.resolve(TestClass.self, scope: .unowned) 62 | let class2 = container.resolver.resolve(TestClass.self, scope: .unowned) 63 | 64 | XCTAssertFalse(class1.id == class2.id) 65 | } 66 | 67 | func testStrongContainer() throws { 68 | let container = Container.services 69 | 70 | var class1: TestClass? = container.resolver.resolve(TestClass.self, scope: .strong) 71 | var class2: TestClass? = container.resolver.resolve(TestClass.self, scope: .strong) 72 | 73 | XCTAssertTrue(class1?.id == class2?.id) 74 | 75 | class1 = nil 76 | 77 | class1 = container.resolver.resolve(TestClass.self, scope: .strong) 78 | 79 | XCTAssertTrue(class1?.id == class2?.id) 80 | 81 | let oldId = class1?.id 82 | 83 | class1 = nil 84 | class2 = nil 85 | 86 | class1 = container.resolver.resolve(TestClass.self, scope: .strong) 87 | class2 = container.resolver.resolve(TestClass.self, scope: .strong) 88 | 89 | XCTAssertTrue(class1?.id == oldId) 90 | XCTAssertTrue(class2?.id == oldId) 91 | } 92 | 93 | func testLabel() throws { 94 | let container = Container.services 95 | 96 | let class1 = container.resolver.resolve(TestClass.self, scope: .weak) 97 | let class2 = container.resolver.resolve(TestClass.self, scope: .weak, label: .test) 98 | 99 | XCTAssertFalse(class1.id == class2.id) 100 | } 101 | } 102 | --------------------------------------------------------------------------------