Skip to content

Commit 7f867ac

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents ddd9af0 + ddcd641 commit 7f867ac

File tree

5 files changed

+78
-37
lines changed

5 files changed

+78
-37
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,20 @@ jobs:
3737
run: make test-${{ matrix.variation }}
3838

3939
wasm:
40-
name: SwiftWasm
40+
name: Wasm
4141
runs-on: ubuntu-latest
42-
strategy:
43-
matrix:
44-
include:
45-
- toolchain: swift-DEVELOPMENT-SNAPSHOT-2024-09-12-a
46-
swift-sdk: swift-wasm-DEVELOPMENT-SNAPSHOT-2024-09-12-a
47-
checksum: 630ce23114580dfae029f832d8ccc8b1ba5136b7f915e82f8e405650e326b562
4842
steps:
4943
- uses: actions/checkout@v4
5044
- uses: bytecodealliance/actions/wasmtime/setup@v1
5145
- name: Install Swift and Swift SDK for WebAssembly
5246
run: |
5347
PREFIX=/opt/swift
54-
SWIFT_TOOLCHAIN_TAG="${{ matrix.toolchain }}"
55-
SWIFT_SDK_TAG="${{ matrix.swift-sdk }}"
5648
set -ex
57-
curl -f -o /tmp/swift.tar.gz "https://download.swift.org/development/ubuntu2204/$SWIFT_TOOLCHAIN_TAG/$SWIFT_TOOLCHAIN_TAG-ubuntu22.04.tar.gz"
49+
curl -f -o /tmp/swift.tar.gz "https://download.swift.org/swift-6.0.2-release/ubuntu2204/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE-ubuntu22.04.tar.gz"
5850
sudo mkdir -p $PREFIX; sudo tar -xzf /tmp/swift.tar.gz -C $PREFIX --strip-component 1
59-
$PREFIX/usr/bin/swift sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip" --checksum ${{ matrix.checksum }}
51+
$PREFIX/usr/bin/swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip --checksum 6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4
6052
echo "$PREFIX/usr/bin" >> $GITHUB_PATH
53+
6154
- name: Build tests
6255
run: swift build --swift-sdk wasm32-unknown-wasi --build-tests -Xlinker -z -Xlinker stack-size=$((1024 * 1024))
6356
- name: Run tests

README.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ SwiftUI, UIKit, AppKit, and even non-Apple platforms.
2929

3030
#### SwiftUI
3131

32-
> [!IMPORTANT]
33-
> To get access to the tools described below you must depend on the SwiftNavigation package and
34-
> import the SwiftUINavigation library.
35-
3632
SwiftUI already comes with incredibly powerful navigation APIs, but there are a few areas lacking
3733
that can be filled. In particular, driving navigation from enum state so that you can have
3834
compile-time guarantees that only one destination can be active at a time.
@@ -96,10 +92,16 @@ This is more concise, and we get compile-time verification that at most one dest
9692
active at a time. However, SwiftUI does not come with the tools to drive navigation from this
9793
model. This is where the SwiftUINavigation tools becomes useful.
9894

95+
> [!IMPORTANT]
96+
> To get access to the tools described below you must depend on the SwiftNavigation package
97+
> (see [Installation](#installation)) and import the **SwiftUINavigation** library.
98+
9999
We start by annotating the `Destination` enum with the `@CasePathable` macro, which allows one to
100100
refer to the cases of an enum with dot-syntax just like one does with structs and properties:
101101

102102
```diff
103+
+import SwiftNavigation
104+
103105
+@CasePathable
104106
enum Destination {
105107
// ...
@@ -110,6 +112,8 @@ And now one can use simple dot-chaining syntax to derive a binding from a partic
110112
the `destination` property:
111113

112114
```swift
115+
import SwiftUINavigation
116+
// ...
113117
.sheet(item: $model.destination.addItem) { addItemModel in
114118
AddItemView(model: addItemModel)
115119
}
@@ -133,7 +137,7 @@ we can still use SwiftUI's navigation APIs.
133137

134138
> [!IMPORTANT]
135139
> To get access to the tools described below you must depend on the SwiftNavigation package and
136-
> import the UIKitNavigation library.
140+
> import the **UIKitNavigation** library.
137141
138142
Unlike SwiftUI, UIKit does not come with state-driven navigation tools. Its navigation tools are
139143
"fire-and-forget", meaning you simply invoke a method to trigger a navigation, but there is
@@ -151,17 +155,20 @@ This makes it easy to get started with navigation, but as SwiftUI has taught us,
151155
powerful to be able to drive navigation from state. It allows you to encapsulate more of your
152156
feature's logic in an isolated and testable domain, and it unlocks deep linking for free since one
153157
just needs to construct a piece of state that represents where you want to navigate to, hand it to
154-
SwiftUI, and let SwiftUI handle the rest.
158+
SwiftUI, and let it handle the rest.
155159

156160
The UIKitNavigation library brings a powerful suite of navigation tools to UIKit that are heavily
157161
inspired by SwiftUI. For example, if you have a feature model like the one discussed above in
158162
the [SwiftUI](#swiftui) section:
159163

160164
```swift
165+
import SwiftNavigation
166+
161167
@Observable
162168
class FeatureModel {
163169
var destination: Destination?
164170

171+
@CasePathable
165172
enum Destination {
166173
case addItem(AddItemModel)
167174
case deleteItemAlert
@@ -173,6 +180,8 @@ class FeatureModel {
173180
…then one can drive navigation in a _view controller_ using tools in the library:
174181

175182
```swift
183+
import UIKitNavigation
184+
176185
class FeatureViewController: UIViewController {
177186
@UIBindable var model: FeatureModel
178187

@@ -184,7 +193,7 @@ class FeatureViewController: UIViewController {
184193
present(item: $model.destination.addItem) { addItemModel in
185194
AddItemViewController(model: addItemModel)
186195
}
187-
present(isPresented: Binding($model.destination.deleteItemAlert)) {
196+
present(isPresented: UIBinding($model.destination.deleteItemAlert)) {
188197
let alert = UIAlertController(title: "Delete?", message: message, preferredStyle: .alert)
189198
alert.addAction(UIAlertAction(title: "Yes", style: .destructive))
190199
alert.addAction(UIAlertAction(title: "No", style: .cancel))

Sources/SwiftNavigation/Internal/KeyPath+Sendable.swift

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,29 @@
77
public typealias _SendableWritableKeyPath<Root, Value> = WritableKeyPath<Root, Value>
88
#endif
99

10-
func sendableKeyPath<Root, Value>(
11-
_ keyPath: KeyPath<Root, Value>
12-
) -> _SendableKeyPath<Root, Value> {
13-
unsafeBitCast(keyPath, to: _SendableKeyPath<Root, Value>.self)
14-
}
10+
// NB: Dynamic member lookup does not currently support sendable key paths and even breaks
11+
// autocomplete.
12+
//
13+
// * https://github.com/swiftlang/swift/issues/77035
14+
// * https://github.com/swiftlang/swift/issues/77105
15+
extension _AppendKeyPath {
16+
@_transparent
17+
func unsafeSendable<Root, Value>() -> _SendableKeyPath<Root, Value>
18+
where Self == KeyPath<Root, Value> {
19+
#if compiler(>=6)
20+
unsafeBitCast(self, to: _SendableKeyPath<Root, Value>.self)
21+
#else
22+
self
23+
#endif
24+
}
1525

16-
func sendableKeyPath<Root, Value>(
17-
_ keyPath: WritableKeyPath<Root, Value>
18-
) -> _SendableWritableKeyPath<Root, Value> {
19-
unsafeBitCast(keyPath, to: _SendableWritableKeyPath<Root, Value>.self)
26+
@_transparent
27+
func unsafeSendable<Root, Value>() -> _SendableWritableKeyPath<Root, Value>
28+
where Self == WritableKeyPath<Root, Value> {
29+
#if compiler(>=6)
30+
unsafeBitCast(self, to: _SendableWritableKeyPath<Root, Value>.self)
31+
#else
32+
self
33+
#endif
34+
}
2035
}

Sources/SwiftNavigation/Observe.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,9 @@ private func onChange(
172172
/// }
173173
/// }
174174
/// ```
175-
public final class ObserveToken: @unchecked Sendable, HashableObject {
175+
public final class ObserveToken: Sendable, HashableObject {
176176
fileprivate let _isCancelled = LockIsolated(false)
177-
public var onCancel: @Sendable () -> Void
177+
public let onCancel: @Sendable () -> Void
178178

179179
public var isCancelled: Bool {
180180
_isCancelled.withValue { $0 }

Sources/SwiftNavigation/UIBinding.swift

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import ConcurrencyExtras
22
import IssueReporting
33

4+
#if canImport(Observation)
5+
import Observation
6+
#endif
7+
48
/// A property wrapper type that can read and write an observable value.
59
///
610
/// Like SwiftUI's `Binding`, but works for UIKit, AppKit, and non-Apple platforms such as
@@ -378,7 +382,7 @@ public struct UIBinding<Value>: Sendable {
378382
) -> UIBinding<Member> {
379383
func open(_ location: some _UIBinding<Value>) -> UIBinding<Member> {
380384
UIBinding<Member>(
381-
location: _UIBindingAppendKeyPath(base: location, keyPath: sendableKeyPath(keyPath)),
385+
location: _UIBindingAppendKeyPath(base: location, keyPath: keyPath.unsafeSendable()),
382386
transaction: transaction
383387
)
384388
}
@@ -396,7 +400,7 @@ public struct UIBinding<Value>: Sendable {
396400
where Value: CasePathable {
397401
func open(_ location: some _UIBinding<Value>) -> UIBinding<Member?> {
398402
UIBinding<Member?>(
399-
location: _UIBindingEnumToOptionalCase(base: location, keyPath: sendableKeyPath(keyPath)),
403+
location: _UIBindingEnumToOptionalCase(base: location, keyPath: keyPath.unsafeSendable()),
400404
transaction: transaction
401405
)
402406
}
@@ -413,7 +417,7 @@ public struct UIBinding<Value>: Sendable {
413417
where Value == Wrapped? {
414418
func open(_ location: some _UIBinding<Value>) -> UIBinding<Member?> {
415419
UIBinding<Member?>(
416-
location: _UIBindingOptionalToMember(base: location, keyPath: sendableKeyPath(keyPath)),
420+
location: _UIBindingOptionalToMember(base: location, keyPath: keyPath.unsafeSendable()),
417421
transaction: transaction
418422
)
419423
}
@@ -430,7 +434,7 @@ public struct UIBinding<Value>: Sendable {
430434
where Value == V? {
431435
func open(_ location: some _UIBinding<Value>) -> UIBinding<Member?> {
432436
UIBinding<Member?>(
433-
location: _UIBindingOptionalEnumToCase(base: location, keyPath: sendableKeyPath(keyPath)),
437+
location: _UIBindingOptionalEnumToCase(base: location, keyPath: keyPath.unsafeSendable()),
434438
transaction: transaction
435439
)
436440
}
@@ -560,14 +564,34 @@ private final class _UIBindingWeakRoot<Root: AnyObject, Value>: _UIBinding, @unc
560564
}
561565
}
562566

563-
@Perceptible
564-
private final class _UIBindingWrapper<Value> {
565-
var value: Value
567+
private final class _UIBindingWrapper<Value>: Perceptible {
568+
var _value: Value
569+
var value: Value {
570+
get {
571+
_$perceptionRegistrar.access(self, keyPath: \.value)
572+
return _value
573+
}
574+
set {
575+
_$perceptionRegistrar.withMutation(of: self, keyPath: \.value) {
576+
_value = newValue
577+
}
578+
}
579+
_modify {
580+
_$perceptionRegistrar.willSet(self, keyPath: \.value)
581+
defer { _$perceptionRegistrar.didSet(self, keyPath: \.value) }
582+
yield &_value
583+
}
584+
}
585+
let _$perceptionRegistrar = PerceptionRegistrar()
566586
init(_ value: Value) {
567-
self.value = value
587+
self._value = value
568588
}
569589
}
570590

591+
#if canImport(Observation)
592+
extension _UIBindingWrapper: Observable {}
593+
#endif
594+
571595
private final class _UIBindingConstant<Value>: _UIBinding, @unchecked Sendable {
572596
let value: Value
573597
init(_ value: Value) {

0 commit comments

Comments
 (0)