├── .gitignore ├── InjectPropertyWrapper.podspec ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── InjectPropertyWrapper │ ├── FatalError.swift │ ├── Inject.swift │ ├── InjectSettings.swift │ └── Resolver.swift └── Tests ├── InjectPropertyWrapperTests ├── Container+Resolver.swift ├── MockObject.swift ├── MockResolver.swift ├── SimpleInjectTests.swift ├── SwinjectInjectTests.swift ├── XCTestCase+FatalError.swift └── XCTestManifests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | .swiftpm/xcode 6 | build/ 7 | DerivedData/ 8 | 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata/ 18 | 19 | *.moved-aside 20 | *.xccheckout 21 | *.xcscmblueprint 22 | 23 | *.hmap 24 | *.ipa 25 | *.dSYM.zip 26 | *.dSYM 27 | -------------------------------------------------------------------------------- /InjectPropertyWrapper.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint InjectPropertyWrapper.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see https://guides.cocoapods.org/syntax/podspec.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |spec| 10 | spec.name = "InjectPropertyWrapper" 11 | spec.version = "0.3.0" 12 | spec.author = { "Peter Verhage" => "peter@egeniq.com" } 13 | spec.homepage = "https://github.com/egeniq/InjectPropertyWrapper" 14 | spec.license = { :type => "MIT", :file => "LICENSE" } 15 | spec.summary = "Provides a Swift @Inject property wrapper to inject objects from a DI framework." 16 | spec.description = <<-DESC 17 | Provides a generic Swift @Inject property wrapper that can be used to inject objects / services from 18 | a dependency injection framework of your choice. 19 | DESC 20 | 21 | spec.swift_version = '5.1' 22 | spec.ios.deployment_target = '9.0' 23 | spec.osx.deployment_target = '10.9' 24 | 25 | spec.source = { :git => "https://github.com/egeniq/InjectPropertyWrapper.git", :tag => "#{spec.version}" } 26 | spec.source_files = "Sources", "Sources/**/*.swift" 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Egeniq 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.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | import Foundation 6 | 7 | // As we only want certain dependencies to be loaded for testing purposes, and there is no 8 | // way to let Swift package manager know to only load the packages for testing purposes, 9 | // we manually set testing to enabled or not to load the test dependencies and target. 10 | let enableTests = ProcessInfo.processInfo.environment["ENABLE_TESTS"] == "1" || 11 | ProcessInfo.processInfo.environment["ENABLE_TESTS"] == "true" || 12 | ProcessInfo.processInfo.environment["ENABLE_TESTS"] == "yes" 13 | 14 | var dependencies: [Package.Dependency] = [] 15 | var targets: [Target] = [ 16 | .target(name: "InjectPropertyWrapper", dependencies: []), 17 | ] 18 | 19 | if enableTests { 20 | dependencies.append( 21 | .package(url: "https://github.com/Swinject/Swinject.git", from: "2.6.2") 22 | ) 23 | 24 | targets.append( 25 | .testTarget( 26 | name: "InjectPropertyWrapperTests", 27 | dependencies: [ 28 | "InjectPropertyWrapper", 29 | "Swinject" 30 | ] 31 | ) 32 | ) 33 | } 34 | 35 | let package = Package( 36 | name: "InjectPropertyWrapper", 37 | products: [ 38 | .library(name: "InjectPropertyWrapper", targets: ["InjectPropertyWrapper"]), 39 | ], 40 | dependencies: dependencies, 41 | targets: targets 42 | ) 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InjectPropertyWrapper 2 | 3 | Provides a generic Swift `@Inject` property wrapper that can be used to inject objects / services from 4 | a dependency injection framework of your choice. 5 | 6 | ## Basic Usage 7 | 8 | First, you need to implement the [`Resolver`](Sources/InjectPropertyWrapper/Resolver.swift) protocol for 9 | the Dependency Injection (DI) framework you are using. 10 | 11 | For example, when using [Swinject](https://github.com/Swinject/Swinject): 12 | ```swift 13 | extension Container: InjectPropertyWrapper.Resolver { 14 | } 15 | ``` 16 | 17 | In case of Swinject the `Container` class already contains a method with the same signature (`resolve(_ type: T, name: String?)`) 18 | as the InjectPropertyWrapper `Resolver` protocol requires. 19 | 20 | Then you need to set the global resolver (for example in your app delegate): 21 | ```swift 22 | let container = Container() 23 | InjectSettings.resolver = container 24 | ``` 25 | 26 | Register some objects in the container: 27 | ```swift 28 | container.register(APIClient.self) { _ in APIClient() } 29 | container.register(MovieRepository.self) { _ in IMDBMovieRepository() } 30 | container.register(MovieRepository.self, name: "netherlands") { _ in IMDBMovieRepository("nl") } 31 | ``` 32 | 33 | Now you can use the `@Inject` property wrapper to inject objects/services in your own classes: 34 | ```swift 35 | class IMDBMovieRepository: MovieRepository { 36 | @Inject private var apiClient: APIClient 37 | 38 | ... 39 | 40 | func fetchTop10(completionHandler: @escaping (movies: [Movie]) -> Void) { 41 | ... 42 | } 43 | } 44 | 45 | class MovieViewModel: BindableObject { 46 | public var didChange = PassthroughSubject() 47 | public private(set) var top10: [Movie]? { 48 | didSet { 49 | didChange.send() 50 | } 51 | } 52 | 53 | @Inject private var movieRepository: MovieRepository 54 | 55 | func load() { 56 | movieRepository.fetchTop10() { [weak self] movies in 57 | self?.top10 = movies 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | It is also possible to inject different objects of the same type using the name parameter: 64 | ```swift 65 | class MovieViewModel: BindableObject { 66 | ... 67 | @Inject private var globalMovieRepository: MovieRepository 68 | @Inject(name: "netherlands") private var nlMovieRepository: MovieRepository 69 | ... 70 | } 71 | ``` 72 | 73 | Normally if the property wrapper is unable to resolve a dependency it will raise a non-recoverable 74 | fatal error. If for some reason you expect an object sometimes to be unavailable in your container, 75 | you can mark the property as optional: 76 | ```swift 77 | class MovieViewModel: BindableObject { 78 | ... 79 | @Inject(name: "germany") private var deMovieRepository: MovieRepository? 80 | ... 81 | } 82 | ``` 83 | 84 | ## Testing 85 | 86 | To run the tests for this package make sure the `ENABLE_TESTS` environment variable is set to `1` or `true`. For example when using the command line: 87 | ``` 88 | ENABLE_TESTS=1 swift test 89 | ``` 90 | This allows the package to only load certain dependencies when testing. 91 | 92 | ## License 93 | 94 | This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file. 95 | -------------------------------------------------------------------------------- /Sources/InjectPropertyWrapper/FatalError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FatalError.swift 3 | // 4 | // 5 | // Created by Peter Verhage on 03/07/2019. 6 | // 7 | // See https://marcosantadev.com/test-swift-fatalerror/ 8 | // 9 | 10 | import Foundation 11 | 12 | 13 | struct FatalErrorUtil { 14 | private static let defaultFatalErrorClosure = { Swift.fatalError($0, file: $1, line: $2) } 15 | static var fatalErrorClosure: (String, StaticString, UInt) -> Never = defaultFatalErrorClosure 16 | 17 | static func replaceFatalError(closure: @escaping (String, StaticString, UInt) -> Never) { 18 | fatalErrorClosure = closure 19 | } 20 | 21 | static func restoreFatalError() { 22 | fatalErrorClosure = defaultFatalErrorClosure 23 | } 24 | } 25 | 26 | func fatalError(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) -> Never { 27 | FatalErrorUtil.fatalErrorClosure(message(), file, line) 28 | } 29 | -------------------------------------------------------------------------------- /Sources/InjectPropertyWrapper/Inject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Inject.swift 3 | // 4 | // 5 | // Created by Peter Verhage on 01/07/2019. 6 | // 7 | 8 | @propertyWrapper 9 | public struct Inject { 10 | public private(set) var wrappedValue: Value 11 | 12 | public init() { 13 | self.init(name: nil, resolver: nil) 14 | } 15 | 16 | public init(name: String? = nil, resolver: Resolver? = nil) { 17 | guard let resolver = resolver ?? InjectSettings.resolver else { 18 | fatalError("Make sure InjectSettings.resolver is set!") 19 | } 20 | 21 | guard let value = resolver.resolve(Value.self, name: name) else { 22 | fatalError("Could not resolve non-optional \(Value.self)") 23 | } 24 | 25 | wrappedValue = value 26 | } 27 | 28 | public init(name: String? = nil, resolver: Resolver? = nil) where Value == Optional { 29 | guard let resolver = resolver ?? InjectSettings.resolver else { 30 | fatalError("Make sure InjectSettings.resolver is set!") 31 | } 32 | 33 | wrappedValue = resolver.resolve(Wrapped.self, name: name) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/InjectPropertyWrapper/InjectSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectSettings.swift 3 | // 4 | // 5 | // Created by Peter Verhage on 01/07/2019. 6 | // 7 | 8 | public struct InjectSettings { 9 | public static var resolver: Resolver? 10 | } 11 | -------------------------------------------------------------------------------- /Sources/InjectPropertyWrapper/Resolver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Resolver.swift 3 | // 4 | // 5 | // Created by Peter Verhage on 01/07/2019. 6 | // 7 | 8 | public protocol Resolver { 9 | func resolve(_ type: T.Type, name: String?) -> T? 10 | } 11 | -------------------------------------------------------------------------------- /Tests/InjectPropertyWrapperTests/Container+Resolver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Container+Resolver.swift 3 | // 4 | // 5 | // Created by Peter Verhage on 02/07/2019. 6 | // 7 | 8 | import Swinject 9 | import InjectPropertyWrapper 10 | 11 | extension Container: InjectPropertyWrapper.Resolver { 12 | // resolve method signature is the same 13 | } 14 | -------------------------------------------------------------------------------- /Tests/InjectPropertyWrapperTests/MockObject.swift: -------------------------------------------------------------------------------- 1 | import struct InjectPropertyWrapper.Inject 2 | 3 | class MockObject { 4 | @Inject var value: T 5 | @Inject(name: "named") var namedValue: T 6 | } 7 | 8 | class MockObjectOptional { 9 | @Inject var value: T? 10 | @Inject(name: "named") var namedValue: T? 11 | } 12 | -------------------------------------------------------------------------------- /Tests/InjectPropertyWrapperTests/MockResolver.swift: -------------------------------------------------------------------------------- 1 | import protocol InjectPropertyWrapper.Resolver 2 | 3 | class MockResolver: Resolver { 4 | public var registry = [String: Any?]() 5 | 6 | private func key(type: Any.Type, name: String? = nil) -> String { 7 | if let name = name { 8 | return key(type: type) + "#" + name 9 | } else { 10 | return String(describing: type) 11 | } 12 | } 13 | 14 | func register(_ type: T.Type, name: String? = nil, value: T) { 15 | registry[key(type: type, name: name)] = value 16 | } 17 | 18 | func resolve(_ type: T.Type, name: String?) -> T? { 19 | return registry[key(type: type, name: name)] as? T 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/InjectPropertyWrapperTests/SimpleInjectTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import InjectPropertyWrapper 3 | 4 | final class SimpleInjectTests: XCTestCase { 5 | func testInject() { 6 | let resolver = MockResolver() 7 | InjectSettings.resolver = resolver 8 | 9 | resolver.register(String.self, value: "a") 10 | resolver.register(String.self, name: "named", value: "b") 11 | resolver.register(Int.self, value: 123) 12 | resolver.register(Int.self, name: "named", value: 456) 13 | 14 | let stringObject = MockObject() 15 | XCTAssertEqual(stringObject.value, "a") 16 | XCTAssertEqual(stringObject.namedValue, "b") 17 | 18 | let intObject = MockObject() 19 | XCTAssertEqual(intObject.value, 123) 20 | XCTAssertEqual(intObject.namedValue, 456) 21 | 22 | // there is no registered bool, but the optional mock object has declared its 23 | // injected properties as optional, so no error is thrown 24 | let boolObject = MockObjectOptional() 25 | XCTAssertEqual(boolObject.value, nil) 26 | XCTAssertEqual(boolObject.namedValue, nil) 27 | 28 | // however, the non optional mock object does require all injected properties 29 | // to be non optional, so we expect an exception here 30 | expectFatalError(expectedMessage: "Could not resolve non-optional Bool") { 31 | let boolObject = MockObject() 32 | XCTAssertEqual(boolObject.value, nil) 33 | XCTAssertEqual(boolObject.namedValue, nil) 34 | } 35 | } 36 | 37 | static var allTests = [ 38 | ("testInject", testInject) 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /Tests/InjectPropertyWrapperTests/SwinjectInjectTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import InjectPropertyWrapper 4 | import Swinject 5 | 6 | final class SwinjectInjectTests: XCTestCase { 7 | func testInject() { 8 | let container = Container() 9 | container.register(String.self) { _ in "a" } 10 | container.register(String.self, name: "named") { _ in "b" } 11 | container.register(Int.self) { _ in 123 } 12 | container.register(Int.self, name: "named") { _ in 456 } 13 | 14 | InjectSettings.resolver = container 15 | 16 | let stringObject = MockObject() 17 | XCTAssertEqual(stringObject.value, "a") 18 | XCTAssertEqual(stringObject.namedValue, "b") 19 | 20 | let intObject = MockObject() 21 | XCTAssertEqual(intObject.value, 123) 22 | XCTAssertEqual(intObject.namedValue, 456) 23 | 24 | // disable logging, because we don't want to confuse the test output 25 | let loggingFunction = Container.loggingFunction 26 | Container.loggingFunction = nil 27 | 28 | // there is no registered bool, but the optional mock object has declared its 29 | // injected properties as optional, so no error is thrown 30 | let boolObject = MockObjectOptional() 31 | XCTAssertEqual(boolObject.value, nil) 32 | XCTAssertEqual(boolObject.namedValue, nil) 33 | 34 | // however, the non optional mock object does require all injected properties 35 | // to be non optional, so we expect an exception here 36 | expectFatalError(expectedMessage: "Could not resolve non-optional Bool") { 37 | let boolObject = MockObject() 38 | XCTAssertEqual(boolObject.value, nil) 39 | XCTAssertEqual(boolObject.namedValue, nil) 40 | } 41 | 42 | Container.loggingFunction = loggingFunction 43 | } 44 | 45 | static var allTests = [ 46 | ("testInject", testInject) 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /Tests/InjectPropertyWrapperTests/XCTestCase+FatalError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Peter Verhage on 03/07/2019. 6 | // 7 | // See https://marcosantadev.com/test-swift-fatalerror/ 8 | // 9 | 10 | import Foundation 11 | 12 | import XCTest 13 | @testable import InjectPropertyWrapper 14 | 15 | extension XCTestCase { 16 | func expectFatalError(expectedMessage: String, testcase: @escaping () -> Void) { 17 | let expectation = self.expectation(description: "expectingFatalError") 18 | var assertionMessage: String? = nil 19 | 20 | FatalErrorUtil.replaceFatalError { message, _, _ in 21 | assertionMessage = message 22 | expectation.fulfill() 23 | self.unreachable() 24 | } 25 | 26 | DispatchQueue.global(qos: .userInitiated).async(execute: testcase) 27 | 28 | waitForExpectations(timeout: 2) { _ in 29 | XCTAssertEqual(assertionMessage, expectedMessage) 30 | FatalErrorUtil.restoreFatalError() 31 | } 32 | } 33 | 34 | private func unreachable() -> Never { 35 | repeat { 36 | RunLoop.current.run() 37 | } while (true) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/InjectPropertyWrapperTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SimpleInjectTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import InjectPropertyWrapperTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += InjectPropertyWrapperTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------