Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SwiftUI environment #13

Merged
merged 8 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,35 @@ set up correctly:
> 🟣 Runtime Warning: Perceptible state was accessed but is not being tracked. Track changes to
> state by wrapping your view in a 'WithPerceptionTracking' view.

### Bindable

SwiftUI's `@Bindable` property wrapper has also been backported to support perceptible objects. You
can simply qualify the property wrapper with the `Perception` module:

```swift
struct FeatureView: View {
@Perception.Bindable var model: FeatureModel

// ...
}
```

### Environment

SwiftUI's `@Environment` property wrapper and `environment` view modifier's support for observation
has also been backported to support perceptible objects using the exact same APIs:

```swift
struct FeatureView: View {
@Environment(Settings.self) var settings

// ...
}

// In some parent view:
.environment(settings)
```

## Community

If you want to discuss this library or have a question about how to use it to solve
Expand Down
74 changes: 74 additions & 0 deletions Sources/Perception/Environment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import SwiftUI

@available(iOS, introduced: 13, obsoleted: 17)
@available(macOS, introduced: 10.15, obsoleted: 14)
@available(tvOS, introduced: 13, obsoleted: 17)
@available(watchOS, introduced: 6, obsoleted: 10)
@available(visionOS, unavailable)
extension Environment {
/// Creates an environment property to read a perceptible object from the environment.
///
/// A backport of SwiftUI's `Environment.init` that takes an observable object.
///
/// - Parameter objectType: The type of the `Perceptible` object to read from the environment.
@_disfavoredOverload
public init(_ objectType: Value.Type) where Value: AnyObject & Perceptible {
self.init(\.[unwrap: \Value.self])
}

/// Creates an environment property to read a perceptible object from the environment, returning
/// `nil` if no corresponding object has been set in the current view's environment.
///
/// A backport of SwiftUI's `Environment.init` that takes an observable object.
///
/// - Parameter objectType: The type of the `Perceptible` object to read from the environment.
@_disfavoredOverload
public init<T: AnyObject & Perceptible>(_ objectType: T.Type) where Value == T? {
self.init(\.[\T.self])
}
}

@available(iOS, introduced: 13, obsoleted: 17)
@available(macOS, introduced: 10.15, obsoleted: 14)
@available(tvOS, introduced: 13, obsoleted: 17)
@available(watchOS, introduced: 6, obsoleted: 10)
@available(visionOS, unavailable)
extension View {
/// Places a perceptible object in the view’s environment.
///
/// A backport of SwiftUI's `View.environment` that takes an observable object.
///
/// - Parameter object: The object to set for this object's type in the environment, or `nil` to
/// clear an object of this type from the environment.
/// - Returns: A view that has the specified object in its environment.
@_disfavoredOverload
public func environment<T: AnyObject & Perceptible>(_ object: T?) -> some View {
self.environment(\.[\T.self], object)
}
}

private struct PerceptibleKey<T: Perceptible>: EnvironmentKey {
static var defaultValue: T? { nil }
}

extension EnvironmentValues {
fileprivate subscript<T: Perceptible>(_: KeyPath<T, T>) -> T? {
get { self[PerceptibleKey<T>.self] }
set { self[PerceptibleKey<T>.self] = newValue }
}

fileprivate subscript<T: Perceptible>(unwrap _: KeyPath<T, T>) -> T {
get {
guard let object = self[\T.self] else {
fatalError(
"""
No perceptible object of type \(T.self) found. A View.environment(_:) for \(T.self) may \
be missing as an ancestor of this view.
"""
)
}
return object
}
set { self[\T.self] = newValue }
}
}