Add PreferenceKey, preference, and onPreferenceChange#57
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Child-to-parent data flow:
PreferenceKey,.preference(key:value:), and.onPreferenceChange(_:perform:). This closes the last Tier A gap apart from accessibility.No bridge involvement
Unlike
GeometryReader, none of this crosses the bridge — a preference is published and reduced entirely within one Swift resolve pass, so the interpreter is untouched by this PR.onPreferenceChangeinstalls a collector for its subtree, resolves it, and reads the reduction. Reduction starts from the key'sdefaultValueso the result is the same whether or not anything published.Two bugs found by running it, not by testing it
Both were invisible to the unit tests I'd written first, and both came out of the demo screen:
Chained observers swallowed each other's keys. An observer scopes a fresh collector to its subtree, and I only forwarded the key it watched — so with
.onPreferenceChange(A)inside.onPreferenceChange(B), B's values were dropped. On screen the max reduced correctly while the collected list stayed empty. Collectors now carry a per-key merge closure (values are type-erased, so re-reducing elsewhere needs the concrete key type captured at record time) and propagate everything upward.Chained observers re-evaluated forever — this hung the app. They share an identity path, and their change-memos shared a storage key.
persistentObjectcan't castPreferenceMemo<Double>toPreferenceMemo<[String]>, so each pass overwrote the other's memo, every delivery looked new, and the callbacks — which write state — never settled. Tapping "Add a row" killed the process. Memos are now keyed by preference type as well as path.The second one has a regression test I verified genuinely catches it: with the fix reverted the callback fires on all 5 evaluation passes instead of once.
Verification
swift test— 7 new tests, 96 total passing: reduction across a subtree, tree order, the default when nothing publishes, delivery-once settling, nested observers, and the two regressions above.Largest published: 90andCollected: row 0, row 1, row 2. Adding a row updates both to120/ four names; removing one drops back. The app stays alive and settles each time.Scope
transformPreference,anchorPreference, andPreferenceKeywith non-Equatablevalues aren't included —onPreferenceChangerequiresEquatableto detect change, as it does in SwiftUI. Note that a delivered preference costs an extra evaluation pass, since the callback normally writes state.