├── .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 |
--------------------------------------------------------------------------------