Skip to content

Commit

Permalink
DIPropertyWrappers for SwiftUI
Browse files Browse the repository at this point in the history
  • Loading branch information
NikSativa committed Aug 15, 2024
1 parent 3ed0182 commit e5fb1f7
Show file tree
Hide file tree
Showing 38 changed files with 546 additions and 354 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.5
// swift-tools-version:5.9
// swiftformat:disable all
import PackageDescription

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct MyApp: App {

// somewhere in the code
struct ContentView: View {
@EnvironmentLazy var api: API
@DIObservedObject var api: API

var body: some View {
Button("Make request") {
Expand Down Expand Up @@ -114,8 +114,9 @@ or if container is shared you can use:
```
of SwiftUI:
```swift
@EnvironmentLazy var api: API
@EnvironmentProvider var dataBase: DataBase
@DIObservedObject var api: API
@DIStateObject var viewState: ReCreatedState
@DIProvider var dataBase: DataBase
```

## Registration
Expand Down
38 changes: 37 additions & 1 deletion Source/Arguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,45 @@ public struct Arguments {
public func first<T>(_: T.Type = T.self) -> T {
return optionalFirst().unsafelyUnwrapped
}

public var count: Int {
return elements.count
}

public var isEmpty: Bool {
return elements.isEmpty
}
}

// MARK: - Sequence

extension Arguments: Sequence {
public func makeIterator() -> some IteratorProtocol {
return ArgumentsIterator(self)
}
}

private final class ArgumentsIterator: IteratorProtocol {
let args: Arguments
var idx = 0

init(_ args: Arguments) {
self.args = args
}

func next() -> Int? {
guard idx < 0 || idx >= args.count else {
return nil
}

defer {
idx += 1
}
return args[idx]
}
}

// MARK: - ExpressibleByArrayLiteral
// MARK: - Arguments + ExpressibleByArrayLiteral

extension Arguments: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: Any...) {
Expand Down
14 changes: 8 additions & 6 deletions Source/Container.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ public final class Container {
for assembly in allAssemblies {
assembly.assemble(with: self)
}

#if os(iOS)
register(ViewControllerFactory.self, options: .transient) { resolver, _ in
Impl.ViewControllerFactory(resolver: resolver)
}
#endif
}

public convenience init(assemblies: Assembly...) {
Expand All @@ -27,6 +21,14 @@ public final class Container {
}
}

#if os(iOS) && canImport(UIKit)
public func registerViewControllerFactory() {
register(ViewControllerFactory.self, options: .transient) { resolver, _ in
Impl.ViewControllerFactory(resolver: resolver)
}
}
#endif

/// Register shared container. Return `true` when correctly registered, otherwise `false`
/// Manually access to shared container is not recommended, but it's possible `InjectSettings.container`
@discardableResult
Expand Down
57 changes: 57 additions & 0 deletions Source/PropertyWrapper/DIPropertyWrapper/DIObservedObject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#if canImport(SwiftUI)
import Combine
import SwiftUI

@propertyWrapper
public struct DIObservedObject<Value: ObservableObject>: DynamicProperty {
@EnvironmentObject
private var container: ObservableResolver
@ObservedObject
private var holder: InstanceHolder<Value> = .init()
private let parametersHolder: EnvParametersHolder = .init()

public var wrappedValue: Value {
return resolveInstance()
}

public init(named name: String? = nil,
with arguments: Arguments? = nil) {
parametersHolder.name = name
parametersHolder.arguments = arguments
}

public var projectedValue: Binding<Value> {
resolveInstance()
return $holder.instance
}

@discardableResult
private func resolveInstance() -> Value {
if let instance = holder.instance {
return instance
}

let instance: Value = container.resolve(named: parametersHolder.name, with: parametersHolder.arguments)
holder.instance = instance
// @ObservedObject is recreated each time the view is redrawn
// parametersHolder.cleanup()
return instance
}
}

private final class InstanceHolder<Value: ObservableObject>: ObservableObject {
private var observers: Set<AnyCancellable> = []

var instance: Value! {
didSet {
if oldValue !== instance {
assert(observers.isEmpty, "Subscribed to `objectWillChange` multiple times. Should never happen.")
observers = []
instance.objectWillChange.sink { [unowned self] _ in
objectWillChange.send()
}.store(in: &observers)
}
}
}
}
#endif
36 changes: 36 additions & 0 deletions Source/PropertyWrapper/DIPropertyWrapper/DIProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#if canImport(SwiftUI)
import Foundation
import SwiftUI

public struct DIProviderOptions {
public let name: String?
public let arguments: Arguments?
}

@propertyWrapper
public struct DIProvider<Value>: DynamicProperty {
@EnvironmentObject
private var container: ObservableResolver
private let parametersHolder: EnvParametersHolder = .init()

public var wrappedValue: Value {
return container.resolve(named: parametersHolder.name, with: parametersHolder.arguments)
}

public init(named name: String? = nil,
with arguments: Arguments? = nil) {
parametersHolder.name = name
parametersHolder.arguments = arguments
}

public var projectedValue: DIProviderOptions {
get {
return .init(name: parametersHolder.name, arguments: parametersHolder.arguments)
}
set {
parametersHolder.name = newValue.name
parametersHolder.arguments = newValue.arguments
}
}
}
#endif
50 changes: 50 additions & 0 deletions Source/PropertyWrapper/DIPropertyWrapper/DIState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#if canImport(SwiftUI)
import Combine
import SwiftUI

@propertyWrapper
public struct DIState<Value>: DynamicProperty {
@EnvironmentObject
private var container: ObservableResolver
@State
private var holder: InstanceHolder<Value> = .init()
private let parametersHolder: EnvParametersHolder = .init()

public var wrappedValue: Value {
return resolveInstance()
}

public init(named name: String? = nil,
with arguments: Arguments? = nil) {
parametersHolder.name = name
parametersHolder.arguments = arguments
}

public var projectedValue: Binding<Value> {
resolveInstance()
return $holder.instance
}

@discardableResult
private func resolveInstance() -> Value {
if let instance = holder.instance {
return instance
}

let instance: Value = container.resolve(named: parametersHolder.name, with: parametersHolder.arguments)
holder.instance = instance
parametersHolder.cleanup()
return instance
}
}

private final class InstanceHolder<Value>: ObservableObject {
var instance: Value! {
didSet {
if oldValue != nil {
objectWillChange.send()
}
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import Combine
import SwiftUI

@propertyWrapper
public struct EnvironmentLazyObject<Value: ObservableObject>: DynamicProperty {
@EnvironmentObject private var container: ObservableResolver
@StateObject private var holder: Holder<Value> = .init()
public struct DIStateObject<Value: ObservableObject>: DynamicProperty {
@EnvironmentObject
private var container: ObservableResolver
@StateObject
private var holder: InstanceHolder<Value> = .init()
private let parametersHolder: EnvParametersHolder = .init()

public var wrappedValue: Value {
return resolveInstance()
}

private let name: String?
private let arguments: Arguments

public init(named name: String? = nil,
with arguments: Arguments = .init()) {
self.name = name
self.arguments = arguments
with arguments: Arguments? = nil) {
parametersHolder.name = name
parametersHolder.arguments = arguments
}

public var projectedValue: Binding<Value> {
Expand All @@ -31,13 +31,14 @@ public struct EnvironmentLazyObject<Value: ObservableObject>: DynamicProperty {
return instance
}

let instance: Value = container.resolve(named: name, with: arguments)
let instance: Value = container.resolve(named: parametersHolder.name, with: parametersHolder.arguments)
holder.instance = instance
parametersHolder.cleanup()
return instance
}
}

private final class Holder<Value: ObservableObject>: ObservableObject {
private final class InstanceHolder<Value: ObservableObject>: ObservableObject {
private var observers: Set<AnyCancellable> = []

var instance: Value! {
Expand Down
13 changes: 13 additions & 0 deletions Source/PropertyWrapper/EnvParametersHolder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#if canImport(SwiftUI)
import Foundation

internal final class EnvParametersHolder {
var name: String?
var arguments: Arguments?

func cleanup() {
name = nil
arguments = nil
}
}
#endif
34 changes: 0 additions & 34 deletions Source/PropertyWrapper/EnvironmentLazy.swift

This file was deleted.

23 changes: 0 additions & 23 deletions Source/PropertyWrapper/EnvironmentProvider.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public struct Inject<Value> {
}
}

public init(named: String? = nil, with arguments: Arguments = .init()) {
public init(named: String? = nil, with arguments: Arguments? = nil) {
guard let resolver = InjectSettings.resolver else {
fatalError("Container is not shared")
}
Expand Down
Loading

0 comments on commit e5fb1f7

Please sign in to comment.