From bdd0b91a2b7d687681d253f8c9a8d754ae709b59 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 12 Jan 2024 11:23:11 -0800 Subject: [PATCH 1/8] Add support for SwiftUI environment --- Example/Example/ExampleApp.swift | 11 ++++- Sources/Perception/Environment.swift | 62 ++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 Sources/Perception/Environment.swift diff --git a/Example/Example/ExampleApp.swift b/Example/Example/ExampleApp.swift index e099ae3..53bd8f8 100644 --- a/Example/Example/ExampleApp.swift +++ b/Example/Example/ExampleApp.swift @@ -1,10 +1,17 @@ import SwiftUI +import Perception @main struct ExampleApp: App { + let model = CounterModel() + var body: some Scene { - WindowGroup { - ContentView(model: CounterModel()) + WithPerceptionTracking { + let _ = print("!!!") +// let _ = model.count + WindowGroup { + ContentView(model: model) + } } } } diff --git a/Sources/Perception/Environment.swift b/Sources/Perception/Environment.swift new file mode 100644 index 0000000..0a38986 --- /dev/null +++ b/Sources/Perception/Environment.swift @@ -0,0 +1,62 @@ +import SwiftUI + +extension Environment { + @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) + @_disfavoredOverload + public init(_ objectType: Value.Type) where Value: AnyObject & Perceptible { + self.init(\.[unwrap: \Value.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) + @_disfavoredOverload + public init(_ objectType: T.Type) where Value == T? { + self.init(\.[\T.self]) + } +} + +extension View { + @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) + public func environment(_ object: T?) -> some View { + self.environment(\.[\T.self], object) + } +} + +private struct PerceptibleKey: EnvironmentKey { + static var defaultValue: T? { nil } +} + +extension EnvironmentValues { + fileprivate subscript(_: KeyPath) -> T? { + get { self[PerceptibleKey.self] } + set { self[PerceptibleKey.self] = newValue } + } +} + +extension EnvironmentValues { + fileprivate subscript(unwrap _: KeyPath) -> 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 } + } +} From ee20e238ec4b6ab386a2eea84a02dd5b888cb140 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 12 Jan 2024 11:47:43 -0800 Subject: [PATCH 2/8] Update ExampleApp.swift --- Example/Example/ExampleApp.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Example/Example/ExampleApp.swift b/Example/Example/ExampleApp.swift index 53bd8f8..5c47763 100644 --- a/Example/Example/ExampleApp.swift +++ b/Example/Example/ExampleApp.swift @@ -3,14 +3,10 @@ import Perception @main struct ExampleApp: App { - let model = CounterModel() - var body: some Scene { WithPerceptionTracking { - let _ = print("!!!") -// let _ = model.count WindowGroup { - ContentView(model: model) + ContentView(model: CounterModel()) } } } From 5600d6aa018524e2555ac8a30a15ca372897aa3f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 12 Jan 2024 11:48:05 -0800 Subject: [PATCH 3/8] Update ExampleApp.swift --- Example/Example/ExampleApp.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Example/Example/ExampleApp.swift b/Example/Example/ExampleApp.swift index 5c47763..e099ae3 100644 --- a/Example/Example/ExampleApp.swift +++ b/Example/Example/ExampleApp.swift @@ -1,13 +1,10 @@ import SwiftUI -import Perception @main struct ExampleApp: App { var body: some Scene { - WithPerceptionTracking { - WindowGroup { - ContentView(model: CounterModel()) - } + WindowGroup { + ContentView(model: CounterModel()) } } } From 977b67382991fb53ef7ef4abf39e6789d24819f9 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 12 Jan 2024 11:48:46 -0800 Subject: [PATCH 4/8] Update Environment.swift --- Sources/Perception/Environment.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Perception/Environment.swift b/Sources/Perception/Environment.swift index 0a38986..763fe63 100644 --- a/Sources/Perception/Environment.swift +++ b/Sources/Perception/Environment.swift @@ -28,6 +28,7 @@ extension View { @available(tvOS, introduced: 13, obsoleted: 17) @available(watchOS, introduced: 6, obsoleted: 10) @available(visionOS, unavailable) + @_disfavoredOverload public func environment(_ object: T?) -> some View { self.environment(\.[\T.self], object) } From 19c6f6f49c58fcf8c63f890ccf9dd8b29d1eeaec Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 12 Jan 2024 11:50:21 -0800 Subject: [PATCH 5/8] Update Environment.swift --- Sources/Perception/Environment.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/Perception/Environment.swift b/Sources/Perception/Environment.swift index 763fe63..45016b5 100644 --- a/Sources/Perception/Environment.swift +++ b/Sources/Perception/Environment.swift @@ -43,9 +43,7 @@ extension EnvironmentValues { get { self[PerceptibleKey.self] } set { self[PerceptibleKey.self] = newValue } } -} -extension EnvironmentValues { fileprivate subscript(unwrap _: KeyPath) -> T { get { guard let object = self[\T.self] else { From 9ffb14f3a1a86f1de41320df872837f7deaa3f01 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 12 Jan 2024 11:50:54 -0800 Subject: [PATCH 6/8] Update Environment.swift --- Sources/Perception/Environment.swift | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/Sources/Perception/Environment.swift b/Sources/Perception/Environment.swift index 45016b5..175ba07 100644 --- a/Sources/Perception/Environment.swift +++ b/Sources/Perception/Environment.swift @@ -1,33 +1,28 @@ 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 { - @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) @_disfavoredOverload public init(_ objectType: Value.Type) where Value: AnyObject & Perceptible { self.init(\.[unwrap: \Value.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) @_disfavoredOverload public init(_ 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 { - @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) @_disfavoredOverload public func environment(_ object: T?) -> some View { self.environment(\.[\T.self], object) From 17b7455eb54627a6201372cc01eac3375a1f5832 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 12 Jan 2024 11:56:46 -0800 Subject: [PATCH 7/8] wip --- Sources/Perception/Environment.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Sources/Perception/Environment.swift b/Sources/Perception/Environment.swift index 175ba07..3b662f2 100644 --- a/Sources/Perception/Environment.swift +++ b/Sources/Perception/Environment.swift @@ -6,11 +6,22 @@ import SwiftUI @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(_ objectType: T.Type) where Value == T? { self.init(\.[\T.self]) @@ -23,6 +34,13 @@ extension Environment { @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(_ object: T?) -> some View { self.environment(\.[\T.self], object) From c9f32082cbc2f127ae3f07b739a32002f407d14d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 12 Jan 2024 12:50:30 -0800 Subject: [PATCH 8/8] README --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index aaff8a0..bc6b4af 100644 --- a/README.md +++ b/README.md @@ -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