Perception 2.0 (#3736)

* Perception 2.0

* wip

* wip

* wip

* wip

* wip

* Revert "wip"

This reverts commit c5d0a06017.

* wip
This commit is contained in:
Stephen Celis
2025-07-30 11:16:38 -07:00
committed by GitHub
parent d83d00c13d
commit af0a2c7408
11 changed files with 89 additions and 153 deletions

View File

@@ -1,5 +1,5 @@
{
"originHash" : "92f2ded678a41ef5d8bc6b77a6f478ed09039d89ffc674e73012e9f30791ecb5",
"originHash" : "0e9a414ac23b15d7d00b87671275679eb6cce1d5dc101ec944f5e8aee4cb097b",
"pins" : [
{
"identity" : "combine-schedulers",
@@ -105,8 +105,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-navigation",
"state" : {
"revision" : "ae208d1a5cf33aee1d43734ea780a09ada6e2a21",
"version" : "2.3.1"
"revision" : "4e89284c1966538109dc783497405bc680e9bc96",
"version" : "2.4.0"
}
},
{
@@ -114,8 +114,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-perception",
"state" : {
"revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01",
"version" : "1.6.0"
"revision" : "f4f57cac7d273cddf0161293d47adbb5a6ba3aed",
"version" : "2.0.0"
}
},
{
@@ -123,8 +123,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-sharing",
"state" : {
"revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9",
"version" : "2.5.2"
"revision" : "5d87dda90ed048f216826efbad404110141161bb",
"version" : "2.6.0"
}
},
{
@@ -150,10 +150,10 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
"version" : "1.5.2"
"revision" : "23e3442166b5122f73f9e3e622cd1e4bafeab3b7",
"version" : "1.6.0"
}
}
],
"version" : 2
"version" : 3
}

View File

@@ -46,70 +46,6 @@ jobs:
- name: Debug
run: make XCODEBUILD_ARGUMENT="${{ matrix.command }}" CONFIG=Debug PLATFORM="${{ matrix.platform }}" WORKSPACE=.github/package.xcworkspace xcodebuild
xcodebuild:
name: xcodebuild (15)
runs-on: macos-14
strategy:
matrix:
command: [test, '']
platform:
- IOS
- MAC_CATALYST
- MACOS
- TVOS
# - VISIONOS # Unfortunately, visionOS on CI is too flakey
- WATCHOS
xcode: [15.2, 15.4]
exclude:
- {xcode: 15.2, command: test}
- {xcode: 15.4, command: ''}
- {xcode: 15.2, platform: MAC_CATALYST}
- {xcode: 15.2, platform: TVOS}
# - {xcode: 15.2, platform: VISIONOS}
- {xcode: 15.2, platform: WATCHOS}
steps:
- uses: actions/checkout@v4
- name: Select Xcode ${{ matrix.xcode }}
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: Update xcbeautify
run: brew update && brew upgrade xcbeautify
- name: Install visionOS runtime
if: matrix.platform == 'visionOS'
run: |
sudo xcodebuild -runFirstLaunch
sudo xcrun simctl list
sudo xcodebuild -downloadPlatform visionOS
sudo xcodebuild -runFirstLaunch
- name: List available devices
run: xcrun simctl list devices available
- name: Cache derived data
uses: actions/cache@v3
with:
path: |
~/.derivedData
key: |
deriveddata-xcodebuild-${{ matrix.platform }}-${{ matrix.xcode }}-${{ matrix.command }}-${{ hashFiles('**/Sources/**/*.swift', '**/Tests/**/*.swift') }}
restore-keys: |
deriveddata-xcodebuild-${{ matrix.platform }}-${{ matrix.xcode }}-${{ matrix.command }}-
- name: Set IgnoreFileSystemDeviceInodeChanges flag
run: defaults write com.apple.dt.XCBuild IgnoreFileSystemDeviceInodeChanges -bool YES
- name: Update mtime for incremental builds
uses: chetan/git-restore-mtime-action@v2
- name: Debug
run: make XCODEBUILD_ARGUMENT="${{ matrix.command }}" CONFIG=Debug PLATFORM="${{ matrix.platform }}" WORKSPACE=.github/package.xcworkspace xcodebuild
library-evolution:
name: Library (evolution)
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Select Xcode 15.4
run: sudo xcode-select -s /Applications/Xcode_15.4.app
- name: Update xcbeautify
run: brew update && brew upgrade xcbeautify
- name: Build for library evolution
run: make build-for-library-evolution
examples:
name: Examples
runs-on: macos-15
@@ -147,15 +83,3 @@ jobs:
run: make DERIVED_DATA_PATH=~/.derivedData SCHEME="Todos" xcodebuild-raw
- name: VoiceMemos
run: make DERIVED_DATA_PATH=~/.derivedData SCHEME="VoiceMemos" xcodebuild-raw
check-macro-compatibility:
name: Check Macro Compatibility
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run Swift Macro Compatibility Check
uses: Matejkob/swift-macro-compatibility-check@v1
with:
run-tests: false
major-versions-only: true

View File

@@ -1,5 +1,5 @@
{
"originHash" : "0479414dc97c4704849540dc64b58b99ce1f1c648e4cb9269d822b21cbc51b7f",
"originHash" : "658be5678358d678b69ea40e4be4814633be8197318d5ac54b97fb40cfb2152b",
"pins" : [
{
"identity" : "combine-schedulers",
@@ -33,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections",
"state" : {
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
"version" : "1.2.0"
"revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341",
"version" : "1.2.1"
}
},
{
@@ -69,8 +69,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-docc-plugin",
"state" : {
"revision" : "d1691545d53581400b1de9b0472d45eb25c19fed",
"version" : "1.4.4"
"revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06",
"version" : "1.4.5"
}
},
{
@@ -105,8 +105,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-navigation",
"state" : {
"revision" : "ae208d1a5cf33aee1d43734ea780a09ada6e2a21",
"version" : "2.3.1"
"revision" : "4e89284c1966538109dc783497405bc680e9bc96",
"version" : "2.4.0"
}
},
{
@@ -114,8 +114,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-perception",
"state" : {
"revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01",
"version" : "1.6.0"
"revision" : "f4f57cac7d273cddf0161293d47adbb5a6ba3aed",
"version" : "2.0.0"
}
},
{
@@ -123,8 +123,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-sharing",
"state" : {
"revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9",
"version" : "2.5.2"
"revision" : "5d87dda90ed048f216826efbad404110141161bb",
"version" : "2.6.0"
}
},
{
@@ -132,8 +132,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
"state" : {
"revision" : "37230a37e83f1b7023be08e1b1a2603fcb1567fb",
"version" : "1.18.4"
"revision" : "d7e40607dcd6bc26543f5d9433103f06e0b28f8f",
"version" : "1.18.6"
}
},
{
@@ -159,10 +159,10 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
"version" : "1.5.2"
"revision" : "23e3442166b5122f73f9e3e622cd1e4bafeab3b7",
"version" : "1.6.0"
}
}
],
"version" : 2
"version" : 3
}

View File

@@ -1,5 +1,5 @@
{
"originHash" : "92f2ded678a41ef5d8bc6b77a6f478ed09039d89ffc674e73012e9f30791ecb5",
"originHash" : "0e9a414ac23b15d7d00b87671275679eb6cce1d5dc101ec944f5e8aee4cb097b",
"pins" : [
{
"identity" : "combine-schedulers",
@@ -33,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections",
"state" : {
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
"version" : "1.2.0"
"revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341",
"version" : "1.2.1"
}
},
{
@@ -69,8 +69,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-docc-plugin",
"state" : {
"revision" : "d1691545d53581400b1de9b0472d45eb25c19fed",
"version" : "1.4.4"
"revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06",
"version" : "1.4.5"
}
},
{
@@ -105,8 +105,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-navigation",
"state" : {
"revision" : "ae208d1a5cf33aee1d43734ea780a09ada6e2a21",
"version" : "2.3.1"
"revision" : "4e89284c1966538109dc783497405bc680e9bc96",
"version" : "2.4.0"
}
},
{
@@ -114,8 +114,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-perception",
"state" : {
"revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01",
"version" : "1.6.0"
"revision" : "f4f57cac7d273cddf0161293d47adbb5a6ba3aed",
"version" : "2.0.0"
}
},
{
@@ -123,8 +123,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-sharing",
"state" : {
"revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9",
"version" : "2.5.2"
"revision" : "5d87dda90ed048f216826efbad404110141161bb",
"version" : "2.6.0"
}
},
{
@@ -132,8 +132,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "37230a37e83f1b7023be08e1b1a2603fcb1567fb",
"version" : "1.18.4"
"revision" : "d7e40607dcd6bc26543f5d9433103f06e0b28f8f",
"version" : "1.18.6"
}
},
{
@@ -150,10 +150,10 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
"version" : "1.5.2"
"revision" : "23e3442166b5122f73f9e3e622cd1e4bafeab3b7",
"version" : "1.6.0"
}
}
],
"version" : 2
"version" : 3
}

View File

@@ -27,7 +27,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.1.0"),
.package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0"),
.package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.3.0"),
.package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"),
.package(url: "https://github.com/pointfreeco/swift-perception", "1.3.4"..<"3.0.0"),
.package(url: "https://github.com/pointfreeco/swift-sharing", "0.1.2"..<"3.0.0"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.3.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"),

View File

@@ -27,7 +27,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.1.0"),
.package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0"),
.package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.3.0"),
.package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"),
.package(url: "https://github.com/pointfreeco/swift-perception", "1.3.4"..<"3.0.0"),
.package(url: "https://github.com/pointfreeco/swift-sharing", "0.1.2"..<"3.0.0"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.3.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"),

View File

@@ -61,9 +61,13 @@ final class RootCore<Root: Reducer>: Core {
self.reducer = reducer
}
func send(_ action: Root.Action) -> Task<Void, Never>? {
_withoutPerceptionChecking {
#if DEBUG
_PerceptionLocals.$skipPerceptionChecking.withValue(true) {
_send(action)
}
#else
_send(action)
}
#endif
}
private func _send(_ action: Root.Action) -> Task<Void, Never>? {
self.bufferedActions.append(action)

View File

@@ -92,6 +92,7 @@ extension Store where State: ObservableState {
public struct _StoreCollection<ID: Hashable & Sendable, State, Action>: RandomAccessCollection {
private let store: Store<IdentifiedArray<ID, State>, IdentifiedAction<ID, Action>>
private let data: IdentifiedArray<ID, State>
private let isInPerceptionTracking = _isInPerceptionTracking
#if swift(<5.10)
@MainActor(unsafe)
@@ -120,28 +121,37 @@ public struct _StoreCollection<ID: Hashable & Sendable, State, Action>: RandomAc
)
return MainActor._assumeIsolated { [uncheckedSelf = UncheckedSendable(self)] in
let `self` = uncheckedSelf.wrappedValue
guard self.data.indices.contains(position)
else {
return Store()
}
let elementID = self.data.ids[position]
let scopeID = self.store.id(state: \.[id: elementID], action: \.[id: elementID])
guard let child = self.store.children[scopeID] as? Store<State, Action>
else {
@MainActor
func open(
_ core: some Core<IdentifiedArray<ID, State>, IdentifiedAction<ID, Action>>
) -> any Core<State, Action> {
IfLetCore(
base: core,
cachedState: self.data[position],
stateKeyPath: \.[id: elementID],
actionKeyPath: \.[id: elementID]
)
var child: Store<State, Action> {
guard self.data.indices.contains(position)
else {
return Store()
}
return self.store.scope(id: scopeID, childCore: open(self.store.core))
let elementID = self.data.ids[position]
let scopeID = self.store.id(state: \.[id: elementID], action: \.[id: elementID])
guard let child = self.store.children[scopeID] as? Store<State, Action>
else {
@MainActor
func open(
_ core: some Core<IdentifiedArray<ID, State>, IdentifiedAction<ID, Action>>
) -> any Core<State, Action> {
IfLetCore(
base: core,
cachedState: self.data[position],
stateKeyPath: \.[id: elementID],
actionKeyPath: \.[id: elementID]
)
}
return self.store.scope(id: scopeID, childCore: open(self.store.core))
}
return child
}
return child
#if DEBUG
return _PerceptionLocals.$isInPerceptionTracking.withValue(self.isInPerceptionTracking) {
child
}
#else
return child
#endif
}
}
}

View File

@@ -483,7 +483,7 @@ extension Store {
@_spi(Internals)
public var _isInPerceptionTracking: Bool {
#if !os(visionOS)
#if DEBUG && !os(visionOS)
return _PerceptionLocals.isInPerceptionTracking
#else
return false

View File

@@ -167,7 +167,13 @@ public final class Store<State, Action>: _Store {
/// it conforms to ``ObservableState``.
/// - Returns: The return value, if any, of the `body` closure.
public func withState<R>(_ body: (_ state: State) -> R) -> R {
_withoutPerceptionChecking { body(self.currentState) }
#if DEBUG
_PerceptionLocals.$skipPerceptionChecking.withValue(true) {
body(self.currentState)
}
#else
body(self.currentState)
#endif
}
/// Sends an action to the store.

View File

@@ -44,18 +44,10 @@ final class StorePerceptionTests: BaseTCATestCase {
}
}
#if DEBUG && !os(visionOS)
let previous = Perception.isPerceptionCheckingEnabled
Perception.isPerceptionCheckingEnabled = true
defer { Perception.isPerceptionCheckingEnabled = previous }
XCTExpectFailure {
render(FeatureView())
} issueMatcher: {
$0.compactDescription == """
failed - Perceptible state was accessed but is not being tracked. Track changes to state by \
wrapping your view in a 'WithPerceptionTracking' view. This must also be done for any \
escaping, trailing closures, such as 'GeometryReader', `LazyVStack` (and all lazy \
views), navigation APIs ('sheet', 'popover', 'fullScreenCover', etc.), and others.
"""
$0.compactDescription.contains("Perceptible state was accessed")
}
#endif
}