Skip to content

Commit

Permalink
Simplified the requirements for adding EnvironmentValues & Make Globa…
Browse files Browse the repository at this point in the history
…lEnvironment settable (#10)

* Make GlobalEnvironment settable

* Simplified the requirements for adding EnvironmentValues

* Fix Unit test

* Update README
  • Loading branch information
hainayanda authored Apr 7, 2024
1 parent 8e386e3 commit 9a2aab8
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 293 deletions.
72 changes: 34 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,9 @@ protocol MyProtocol {
}

// add to EnvironmentValues
@EnvironmentValue("myValue")
@EnvironmentValue
extension EnvironmentValues {
struct MyProtocolKey: EnvironmentKey {
static let defaultValue = MyProtocolStub()
}
static let myValue: MyProtocol = MyProtocolStub()
}
```

Expand All @@ -79,18 +77,45 @@ Different than SwiftUI Environment, GlobalEnvironment can be injected and access
GlobalResolver.environment(\.myValue, MyImplementation())
```

### GlobalEnvironment

`GlobalEnvironment` complements SwiftUI Environment. It allows `EnvironmentValues` to be accessed globally outside of SwiftUI injection scope. To use it, add EnvironmentValues just like how we add it for SwiftUI, and inject it into `GlobalResolver`:

```swift
@GlobalEnvironment(\.myValue) var myValue
```

To provide gobal environment use GlobalResolver static methods:

```swift
GlobalResolver
.environment(\.myValue, SomeDependency())
```

You can connect multiple KeyPaths to one KeyPaths by a single call:

```swift
GlobalResolver
.environment(\.this, \.that, use: \.myValue)
```

To resolve dependency manually from GlobalResolver, do this:

```swift
let myValue = GlobalResolver.resolve(\.myValue)
```


### EnvironmentValue macro

The `EnvironmentValue` macro is used to remove boilerplate code when adding a new variable to EnvironmentValue. To use it, simply add `@EnvironmentValue("<name of the value KeyPath>")` to the extension of `EnvironmentValues` with the structure of your `EnvironmentKey`.
The `EnvironmentValue` macro is used to remove boilerplate code when adding a new variable to EnvironmentValue. To use it, simply add `@EnvironmentValue` to the extension of `EnvironmentValues` with static variable of your default environmentValue.

```swift
import SwiftEnvironment

@EnvironmentValue("myValue")
@EnvironmentValue
extension EnvironmentValues {
struct MyEnvironmentKey: EnvironmentKey {
static let defaultValue = MyDependency()
}
static let myValue: Dependency = MyDependency()
}
```

Expand Down Expand Up @@ -178,35 +203,6 @@ struct MyStruct {

You can provide multiple types as variadic parameters.

### GlobalEnvironment

`GlobalEnvironment` complements SwiftUI Environment. It allows `EnvironmentValues` to be accessed globally outside of SwiftUI injection scope. To use it, add EnvironmentValues just like how we add it for SwiftUI, and inject it into `GlobalResolver`:

```swift
GlobalResolver
.environment(\.myValue, SomeDependency())
```

Then, you can access it globally using `GlobalEnvironment` property wrapper:

```swift
@GlobalEnvironment(\.myValue) var myValue
```

You can connect multiple KeyPaths to one KeyPaths by a single call:

```swift
GlobalResolver
.environment(\.this, \.that, use: \.myValue)
```

To resolve dependency manually from GlobalResolver, do this:

```swift
let myValue = GlobalResolver.resolve(\.myValue)
```


### GlobalResolver environment

Injected values to `GlobalResolver.environment` are injected using `autoclosure`, so the value will be created lazily. This value will be stored as long as the app is alive. You can inject an explicit closure too if needed:
Expand Down
6 changes: 5 additions & 1 deletion Sources/SwiftEnvironment/GlobalEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ public final class GlobalEnvironment<Value> {
private let resolver: EnvironmentValuesResolver
private let keyPath: KeyPath<EnvironmentValues, Value>

public private(set) lazy var wrappedValue: Value = resolver.resolve(keyPath)
private lazy var _wrappedValue: Value = resolver.resolve(keyPath)
public var wrappedValue: Value {
get { _wrappedValue }
set { _wrappedValue = newValue }
}

public init(_ keyPath: KeyPath<EnvironmentValues, Value>) {
self.keyPath = keyPath
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftEnvironment/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

@attached(member, names: arbitrary)
public macro EnvironmentValue(_ keys: String...) = #externalMacro(
public macro EnvironmentValue() = #externalMacro(
module: "SwiftEnvironmentMacro", type: "EnvironmentValueMacro"
)

Expand Down
157 changes: 38 additions & 119 deletions Sources/SwiftEnvironmentMacro/EnvironmentValueMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,52 @@ public struct EnvironmentValueMacro: MemberMacro {
extensionDeclaration.extendedType.as(IdentifierTypeSyntax.self)?.name.text == "EnvironmentValues" else {
throw EnvironmentValueMacroError.attachedToInvalidType
}
let keyPaths = try node.environmentValueKeyPaths
let environmentKeys = try extensionDeclaration.environmentKeys
return try combine(keyPaths, with: environmentKeys).map { keyPath, environmentKey in
let keyName = environmentKey.name.text
return """
var \(raw: keyPath): \(raw: try environmentKey.environmentValueType) {
get { self[\(raw: keyName).self] }
set { self[\(raw: keyName).self] = newValue }
}
"""
}
return extensionDeclaration.environmentDeclaration
.map { "\(raw: $0)" }
}
}

// MARK: Private functions

private func combine(
_ keyPaths: [String],
with environmentKeys: [StructDeclSyntax]) -> [(keyPath: String, environmentKey: StructDeclSyntax)] {
keyPaths.enumerated().reduce(into: []) { partialResult, pair in
partialResult.append((pair.element, environmentKeys[pair.offset]))
struct EnvironmentDeclaration: CustomStringConvertible {
let baseName: String
let type: String

var derivedName: String {
"\(baseName.firstCapitalized)SwiftEnvironmentKey"
}

var description: String {
"""
struct \(derivedName): EnvironmentKey {
static let defaultValue: \(type) = EnvironmentValues.\(baseName)
}
var \(baseName): \(type) {
get {
self[\(derivedName).self]
}
set {
self[\(derivedName).self] = newValue
}
}
"""
}
}

// MARK: Private extensions

private extension Array where Element: Hashable {
var hasDuplication: Bool {
Set(self).count > self.count
private extension ExtensionDeclSyntax {
var environmentDeclaration: [EnvironmentDeclaration] {
memberBlock.members
.compactMap { $0.decl.as(VariableDeclSyntax.self) }
.filter { $0.isStatic }
.compactMap { variable in
guard let name = variable.name,
let type = variable.typeAnnotation ?? variable.initializer else {
return nil
}
return EnvironmentDeclaration(baseName: name, type: type)

}
}
}

Expand Down Expand Up @@ -78,101 +95,3 @@ private extension VariableDeclSyntax {
.trimmedDescription
}
}

private extension TypeAliasDeclSyntax {

var realType: String? {
initializer.value
.as(IdentifierTypeSyntax.self)?
.name.text
}
}

private extension StructDeclSyntax {

var typeAliasValueRealType: String? {
memberBlock.members
.compactMap { $0.decl.as(TypeAliasDeclSyntax.self) }
.first { $0.name.text == "Value" }?
.realType
}

var environmentValueType: String {
get throws {
let defaultValue = memberBlock.members
.compactMap { $0.decl.as(VariableDeclSyntax.self) }
.first { $0.isStatic && $0.name == "defaultValue" }

guard let defaultValue else {
throw EnvironmentValueMacroError.undeterminedEnvironmentValueType
}
if let typeAnnotation = defaultValue.typeAnnotation,
typeAnnotation != "Value" {
return typeAnnotation
}
if let typeAliasValueRealType {
return typeAliasValueRealType
}
if let initializer = defaultValue.initializer,
initializer != "Value" {
return initializer
}
throw EnvironmentValueMacroError.undeterminedEnvironmentValueType
}
}
}

private extension ExtensionDeclSyntax {
var environmentKeys: [StructDeclSyntax] {
get throws {
let result = memberBlock.members
.compactMap { $0.as(MemberBlockItemSyntax.self)?.decl.as(StructDeclSyntax.self) }
.filter { $0.isEnvironmentKey }

guard !result.isEmpty else {
throw EnvironmentValueMacroError.noEnvironmentKeyProvided
}
let names = result.map { $0.name.text }

guard !names.hasDuplication else {
throw EnvironmentValueMacroError.duplicatedEnvironmentKeys
}
return result
}
}
}

private extension StructDeclSyntax {
var isEnvironmentKey: Bool {
inheritanceClause?.inheritedTypes
.contains {
$0.type.as(IdentifierTypeSyntax.self)?.name.text == "EnvironmentKey"
}
?? false
}
}

private extension AttributeSyntax {

var environmentValueKeyPaths: [String] {
get throws {
guard let labeledSyntaxes = arguments?.as(LabeledExprListSyntax.self) else {
throw EnvironmentValueMacroError.noArgumentPassed
}
let result = try labeledSyntaxes.map {
guard let stringLiteral = $0.expression.as(StringLiteralExprSyntax.self),
let stringSegment = stringLiteral.segments.first?.as(StringSegmentSyntax.self)else {
throw EnvironmentValueMacroError.wrongArgumentValue
}
return stringSegment.content.text
}
guard !result.isEmpty else {
throw EnvironmentValueMacroError.noArgumentPassed
}
guard !result.hasDuplication else {
throw EnvironmentValueMacroError.duplicatedKeyPaths
}
return result
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,12 @@
import Foundation

public enum EnvironmentValueMacroError: CustomStringConvertible, Error {
case noArgumentPassed
case noKeyPathProvided
case wrongArgumentValue
case attachedToInvalidType
case noEnvironmentKeyProvided
case duplicatedKeyPaths
case duplicatedEnvironmentKeys
case undeterminedEnvironmentValueType
case missingEnvironmentKeyValuePair

public var description: String {
switch self {
case .noArgumentPassed:
return "@EnvironmentValue can only be applied with arguments passed"
case .noKeyPathProvided:
return "@EnvironmentValue can only be applied with provided name of KeyPath"
case .wrongArgumentValue:
return "@EnvironmentValue argument value is wrong"
case .attachedToInvalidType:
return "@EnvironmentValue can only be attached to extension of EnvironmentValues"
case .noEnvironmentKeyProvided:
return "@EnvironmentValue failed to extract EnvironmentKey"
case .duplicatedKeyPaths:
return "@EnvironmentValue provided KeyPaths is duplicated"
case .undeterminedEnvironmentValueType:
return "@EnvironmentValue cannot determine EnvironmentValue type"
case .duplicatedEnvironmentKeys:
return "@EnvironmentValue provided EnvironmentKeys is duplicated"
case .missingEnvironmentKeyValuePair:
return "@EnvironmentValue provided KeyPaths number is not the same as provided EnvironmentKeys"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ extension String {
return false
}
}

var firstCapitalized: String { return prefix(1).capitalized + dropFirst() }
}
Loading

0 comments on commit 9a2aab8

Please sign in to comment.