Skip to content

Commit 5c8f562

Browse files
Bring back actor proxy for observe isolation (#316)
* Bring back actor proxy for `observe` isolation While `NSObject.observe` still behaved just fine, `SwiftNavigation.observe` could execute on the wrong actor. * Use @_inheritActorContext @isolated(any).isolation instead of #isolation macro (#317) * Use @_inheritActorContext @isolated(any).isolation instead of #isolation macro * Fix IsolationTests * wip --------- Co-authored-by: Maxim Krouk <[email protected]>
1 parent 5e6f6b8 commit 5c8f562

File tree

5 files changed

+70
-37
lines changed

5 files changed

+70
-37
lines changed

Package.resolved

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ let package = Package(
5353
.testTarget(
5454
name: "SwiftNavigationTests",
5555
dependencies: [
56-
"SwiftNavigation"
56+
"SwiftNavigation",
57+
.product(name: "IssueReportingTestSupport", package: "xctest-dynamic-overlay"),
5758
]
5859
),
5960
.target(
@@ -67,7 +68,8 @@ let package = Package(
6768
.testTarget(
6869
name: "SwiftUINavigationTests",
6970
dependencies: [
70-
"SwiftUINavigation"
71+
"SwiftUINavigation",
72+
.product(name: "IssueReportingTestSupport", package: "xctest-dynamic-overlay"),
7173
]
7274
),
7375
.target(
@@ -91,7 +93,8 @@ let package = Package(
9193
.testTarget(
9294
name: "UIKitNavigationTests",
9395
dependencies: [
94-
"UIKitNavigation"
96+
"UIKitNavigation",
97+
.product(name: "IssueReportingTestSupport", package: "xctest-dynamic-overlay"),
9598
]
9699
),
97100
]

[email protected]

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ let package = Package(
5353
.testTarget(
5454
name: "SwiftNavigationTests",
5555
dependencies: [
56-
"SwiftNavigation"
56+
"SwiftNavigation",
57+
.product(name: "IssueReportingTestSupport", package: "xctest-dynamic-overlay"),
5758
]
5859
),
5960
.target(
@@ -67,7 +68,8 @@ let package = Package(
6768
.testTarget(
6869
name: "SwiftUINavigationTests",
6970
dependencies: [
70-
"SwiftUINavigation"
71+
"SwiftUINavigation",
72+
.product(name: "IssueReportingTestSupport", package: "xctest-dynamic-overlay"),
7173
]
7274
),
7375
.target(
@@ -91,7 +93,8 @@ let package = Package(
9193
.testTarget(
9294
name: "UIKitNavigationTests",
9395
dependencies: [
94-
"UIKitNavigation"
96+
"UIKitNavigation",
97+
.product(name: "IssueReportingTestSupport", package: "xctest-dynamic-overlay"),
9598
]
9699
),
97100
],

Sources/SwiftNavigation/Observe.swift

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,13 @@ import ConcurrencyExtras
5656
/// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
5757
/// is deallocated.
5858
public func observe(
59-
@_inheritActorContext _ apply: @escaping @isolated(any) @Sendable () -> Void
59+
@_inheritActorContext
60+
_ apply: @escaping @isolated(any) @Sendable () -> Void
6061
) -> ObserveToken {
61-
observe { _ in Result(catching: apply).get() }
62+
_observe(
63+
isolation: apply.isolation,
64+
{ _ in Result(catching: apply).get() }
65+
)
6266
}
6367

6468
/// Tracks access to properties of an observable model.
@@ -75,24 +79,34 @@ import ConcurrencyExtras
7579
_ apply: @escaping @isolated(any) @Sendable (_ transaction: UITransaction) -> Void
7680
) -> ObserveToken {
7781
_observe(
78-
apply,
79-
task: { transaction, operation in
80-
Task {
81-
await operation()
82-
}
83-
}
82+
isolation: apply.isolation,
83+
apply
8484
)
8585
}
8686
#endif
8787

8888
func _observe(
89-
_ apply: @escaping @Sendable (_ transaction: UITransaction) -> Void,
90-
task:
91-
@escaping @Sendable (
92-
_ transaction: UITransaction, _ operation: @escaping @Sendable () -> Void
93-
) -> Void = {
94-
Task(operation: $1)
89+
isolation: (any Actor)?,
90+
_ apply: @escaping @Sendable (_ transaction: UITransaction) -> Void
91+
) -> ObserveToken {
92+
let actor = ActorProxy(base: isolation)
93+
return _observe(
94+
apply,
95+
task: { transaction, operation in
96+
Task {
97+
await actor.perform {
98+
operation()
99+
}
100+
}
95101
}
102+
)
103+
}
104+
105+
func _observe(
106+
_ apply: @escaping @Sendable (_ transaction: UITransaction) -> Void,
107+
task: @escaping @Sendable (
108+
_ transaction: UITransaction, _ operation: @escaping @Sendable () -> Void
109+
) -> Void
96110
) -> ObserveToken {
97111
let token = ObserveToken()
98112
onChange(
@@ -124,10 +138,9 @@ func _observe(
124138

125139
private func onChange(
126140
_ apply: @escaping @Sendable (_ transaction: UITransaction) -> Void,
127-
task:
128-
@escaping @Sendable (
129-
_ transaction: UITransaction, _ operation: @escaping @isolated(any) @Sendable () -> Void
130-
) -> Void
141+
task: @escaping @Sendable (
142+
_ transaction: UITransaction, _ operation: @escaping @Sendable () -> Void
143+
) -> Void
131144
) {
132145
withPerceptionTracking {
133146
apply(.current)
@@ -200,3 +213,16 @@ public final class ObserveToken: Sendable, HashableObject {
200213
set.insert(self)
201214
}
202215
}
216+
217+
private actor ActorProxy {
218+
let base: (any Actor)?
219+
init(base: (any Actor)?) {
220+
self.base = base
221+
}
222+
nonisolated var unownedExecutor: UnownedSerialExecutor {
223+
(base ?? MainActor.shared).unownedExecutor
224+
}
225+
func perform(_ operation: @Sendable () -> Void) {
226+
operation()
227+
}
228+
}

Tests/SwiftNavigationTests/ButtonStateTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
2828
Asynchronous actions cannot be animated. Evaluate this action in a synchronous closure, \
2929
or use 'SwiftUI.withAnimation' explicitly.
30-
""")
30+
"""
31+
)
3132
}
3233
}
3334
}

0 commit comments

Comments
 (0)