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

Preserve @Bindable's binding identity #56

Merged
merged 1 commit into from
Apr 4, 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
22 changes: 15 additions & 7 deletions Sources/Perception/Bindable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
@dynamicMemberLookup
@propertyWrapper
public struct Bindable<Value> {
private let binding: UncheckedSendable<Binding<Value>>

/// The wrapped object.
public var wrappedValue: Value
public var wrappedValue: Value {
get { self.binding.value.wrappedValue }
set { self.binding.value.wrappedValue = newValue }
}

/// The bindable wrapper for the object that creates bindings to its properties using dynamic
/// member lookup.
Expand All @@ -26,20 +31,23 @@
public subscript<Subject>(
dynamicMember keyPath: ReferenceWritableKeyPath<Value, Subject>
) -> Binding<Subject> where Value: AnyObject {
Binding(
get: { self.wrappedValue[keyPath: keyPath] },
set: { self.wrappedValue[keyPath: keyPath] = $0 }
)
self.binding.value[dynamicMember: keyPath]
}

/// Creates a bindable object from an observable object.
public init(wrappedValue: Value) where Value: AnyObject & Perceptible {
self.wrappedValue = wrappedValue
var value = wrappedValue
self.binding = UncheckedSendable(
Binding(
get: { value },
set: { value = $0 }
)
)
}

/// Creates a bindable object from an observable object.
public init(_ wrappedValue: Value) where Value: AnyObject & Perceptible {
self.wrappedValue = wrappedValue
self.init(wrappedValue: wrappedValue)
}

/// Creates a bindable from the value of another bindable.
Expand Down
6 changes: 6 additions & 0 deletions Sources/Perception/Internal/UncheckedSendable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
struct UncheckedSendable<Value>: @unchecked Sendable {
let value: Value
init(_ value: Value) {
self.value = value
}
}
7 changes: 0 additions & 7 deletions Sources/Perception/WithPerceptionTracking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,3 @@ public enum _PerceptionLocals {
@TaskLocal public static var isInPerceptionTracking = false
@TaskLocal public static var skipPerceptionChecking = false
}

private struct UncheckedSendable<A>: @unchecked Sendable {
let value: A
init(_ value: A) {
self.value = value
}
}
35 changes: 34 additions & 1 deletion Tests/PerceptionTests/RuntimeWarningTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@
import XCTest

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
@MainActor
final class RuntimeWarningTests: XCTestCase {
@MainActor
func testNotInPerceptionBody() {
let model = Model()
model.count += 1
XCTAssertEqual(model.count, 1)
}

@MainActor
func testInPerceptionBody_NotInSwiftUIBody() {
let model = Model()
_PerceptionLocals.$isInPerceptionTracking.withValue(true) {
_ = model.count
}
}

@MainActor
func testNotInPerceptionBody_InSwiftUIBody() {
struct FeatureView: View {
let model = Model()
Expand All @@ -30,6 +32,7 @@
self.render(FeatureView())
}

@MainActor
func testNotInPerceptionBody_InSwiftUIBody_Wrapper() {
struct FeatureView: View {
let model = Model()
Expand All @@ -42,6 +45,7 @@
self.render(FeatureView())
}

@MainActor
func testInPerceptionBody_InSwiftUIBody_Wrapper() {
struct FeatureView: View {
let model = Model()
Expand All @@ -56,6 +60,7 @@
self.render(FeatureView())
}

@MainActor
func testInPerceptionBody_InSwiftUIBody() {
struct FeatureView: View {
let model = Model()
Expand All @@ -68,6 +73,7 @@
self.render(FeatureView())
}

@MainActor
func testNotInPerceptionBody_SwiftUIBinding() {
struct FeatureView: View {
@State var model = Model()
Expand All @@ -80,6 +86,7 @@
self.render(FeatureView())
}

@MainActor
func testInPerceptionBody_SwiftUIBinding() {
struct FeatureView: View {
@State var model = Model()
Expand All @@ -92,6 +99,7 @@
self.render(FeatureView())
}

@MainActor
func testNotInPerceptionBody_ForEach() {
struct FeatureView: View {
@State var model = Model(
Expand All @@ -111,6 +119,7 @@
self.render(FeatureView())
}

@MainActor
func testInnerInPerceptionBody_ForEach() {
struct FeatureView: View {
@State var model = Model(
Expand All @@ -132,6 +141,7 @@
self.render(FeatureView())
}

@MainActor
func testOuterInPerceptionBody_ForEach() {
struct FeatureView: View {
@State var model = Model(
Expand All @@ -153,6 +163,7 @@
self.render(FeatureView())
}

@MainActor
func testOuterAndInnerInPerceptionBody_ForEach() {
struct FeatureView: View {
@State var model = Model(
Expand All @@ -176,6 +187,7 @@
self.render(FeatureView())
}

@MainActor
func testNotInPerceptionBody_Sheet() {
struct FeatureView: View {
@State var model = Model(child: Model())
Expand All @@ -190,6 +202,7 @@
self.render(FeatureView())
}

@MainActor
func testInnerInPerceptionBody_Sheet() {
struct FeatureView: View {
@State var model = Model(child: Model())
Expand All @@ -206,6 +219,7 @@
self.render(FeatureView())
}

@MainActor
func testOuterInPerceptionBody_Sheet() {
struct FeatureView: View {
@State var model = Model(child: Model())
Expand All @@ -222,6 +236,7 @@
self.render(FeatureView())
}

@MainActor
func testOuterAndInnerInPerceptionBody_Sheet() {
struct FeatureView: View {
@State var model = Model(child: Model())
Expand All @@ -240,6 +255,7 @@
self.render(FeatureView())
}

@MainActor
func testActionClosure() {
struct FeatureView: View {
@State var model = Model()
Expand All @@ -252,6 +268,7 @@
self.render(FeatureView())
}

@MainActor
func testActionClosure_CallMethodWithArguments() {
struct FeatureView: View {
@State var model = Model()
Expand All @@ -268,6 +285,7 @@
self.render(FeatureView())
}

@MainActor
func testActionClosure_WithArguments() {
struct FeatureView: View {
@State var model = Model()
Expand All @@ -282,6 +300,7 @@
self.render(FeatureView())
}

@MainActor
func testActionClosure_WithArguments_ImplicitClosure() {
struct FeatureView: View {
@State var model = Model()
Expand All @@ -297,6 +316,7 @@
self.render(FeatureView())
}

@MainActor
func testImplicitActionClosure() {
struct FeatureView: View {
@State var model = Model()
Expand All @@ -312,6 +332,7 @@
self.render(FeatureView())
}

@MainActor
func testRegistrarDisablePerceptionTracking() {
struct FeatureView: View {
let model = Model()
Expand All @@ -324,6 +345,7 @@
self.render(FeatureView())
}

@MainActor
func testGlobalDisablePerceptionTracking() {
let previous = Perception.isPerceptionCheckingEnabled
Perception.isPerceptionCheckingEnabled = false
Expand All @@ -338,6 +360,7 @@
self.render(FeatureView())
}

@MainActor
func testParentAccessingChildState_ParentNotObserving_ChildObserving() {
struct ChildView: View {
let model: Model
Expand Down Expand Up @@ -367,6 +390,7 @@
self.render(FeatureView())
}

@MainActor
func testParentAccessingChildState_ParentObserving_ChildNotObserving() {
struct ChildView: View {
let model: Model
Expand Down Expand Up @@ -394,6 +418,7 @@
self.render(FeatureView())
}

@MainActor
func testParentAccessingChildState_ParentNotObserving_ChildNotObserving() {
struct ChildView: View {
let model: Model
Expand Down Expand Up @@ -421,6 +446,7 @@
self.render(FeatureView())
}

@MainActor
func testParentAccessingChildState_ParentObserving_ChildObserving() {
struct ChildView: View {
let model: Model
Expand Down Expand Up @@ -450,6 +476,7 @@
self.render(FeatureView())
}

@MainActor
func testAccessInOnAppearWithAsyncTask() async throws {
@MainActor
struct FeatureView: View {
Expand All @@ -465,6 +492,7 @@
try await Task.sleep(for: .milliseconds(100))
}

@MainActor
func testAccessInOnAppearWithAsyncTask_Implicit() async throws {
@MainActor
struct FeatureView: View {
Expand All @@ -484,6 +512,7 @@
try await Task.sleep(for: .milliseconds(100))
}

@MainActor
func testAccessInTask() async throws {
@MainActor
struct FeatureView: View {
Expand All @@ -499,6 +528,7 @@
try await Task.sleep(for: .milliseconds(100))
}

@MainActor
func testGeometryReader_WithoutPerceptionTracking() {
struct FeatureView: View {
let model = Model()
Expand All @@ -513,6 +543,7 @@
self.render(FeatureView())
}

@MainActor
func testGeometryReader_WithProperPerceptionTracking() {
struct FeatureView: View {
let model = Model()
Expand All @@ -527,6 +558,7 @@
self.render(FeatureView())
}

@MainActor
func testGeometryReader_ComputedProperty_ImproperPerceptionTracking() {
struct FeatureView: View {
let model = Model()
Expand All @@ -544,6 +576,7 @@
self.render(FeatureView())
}

@MainActor
private func render(_ view: some View) {
let image = ImageRenderer(content: view).cgImage
_ = image
Expand Down
Loading