Skip to content

Commit 882b81c

Browse files
committed
Add PersistentKeyValues AsyncSequence observation
1 parent 37c1a08 commit 882b81c

36 files changed

Lines changed: 1743 additions & 94 deletions

File tree

.github/workflows/deploy_documentation.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ jobs:
2323
environment:
2424
name: github-pages
2525
url: ${{ steps.deployment.outputs.page_url }}
26-
runs-on: macos-15
26+
runs-on: macos-26
2727
steps:
2828
- name: Checkout
29-
uses: actions/checkout@v3
29+
uses: actions/checkout@v4
30+
- name: Select Xcode
31+
run: sudo xcode-select -s /Applications/Xcode_26.4.app
3032
- name: Set Up GitHub Pages
31-
uses: actions/configure-pages@v3
33+
uses: actions/configure-pages@v5
3234
- name: Build Documentation
3335
run: |
3436
xcodebuild docbuild \
@@ -44,7 +46,7 @@ jobs:
4446
run: |
4547
echo "<script>window.location.href += \"/documentation/persistentkeyvaluekit\"</script>" > _site/index.html;
4648
- name: Upload Documentation Artifact to GitHub Pages
47-
uses: actions/upload-pages-artifact@v1
49+
uses: actions/upload-pages-artifact@v3
4850
- name: Deploy to GitHub Pages
4951
id: deployment
50-
uses: actions/deploy-pages@v2
52+
uses: actions/deploy-pages@v4

.github/workflows/test.yml

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ jobs:
1616
strategy:
1717
matrix:
1818
include:
19-
- os: macos-15
20-
xcode: '16.0'
19+
- os: macos-26
20+
xcode: '26.4'
2121
name: Get Environment Details (Xcode ${{ matrix.xcode }})
2222
runs-on: ${{ matrix.os }}
2323
steps:
@@ -37,34 +37,34 @@ jobs:
3737
strategy:
3838
matrix:
3939
include:
40-
- os: macos-15
41-
xcode: '16.0'
40+
- os: macos-26
41+
xcode: '26.4'
4242
platform: iOS
43-
destination: "name=iPhone 16 Pro"
43+
destination: "name=iPhone 17 Pro"
4444
sdk: iphonesimulator
45-
- os: macos-15
46-
xcode: '16.0'
45+
- os: macos-26
46+
xcode: '26.4'
4747
platform: tvOS
4848
destination: "name=Apple TV 4K (3rd generation)"
4949
sdk: appletvsimulator
50-
- os: macos-15
51-
xcode: '16.0'
50+
- os: macos-26
51+
xcode: '26.4'
5252
platform: visionOS
5353
destination: "name=Apple Vision Pro"
5454
sdk: xrsimulator
55-
- os: macos-15
56-
xcode: '16.0'
55+
- os: macos-26
56+
xcode: '26.4'
5757
platform: watchOS
58-
destination: "name=Apple Watch Ultra 2 (49mm)"
58+
destination: "name=Apple Watch Ultra 3 (49mm)"
5959
sdk: watchsimulator
60-
- os: macos-15
61-
xcode: '16.0'
60+
- os: macos-26
61+
xcode: '26.4'
6262
platform: macOS
6363
name: Test ${{ matrix.platform }} (Xcode ${{ matrix.xcode }})
6464
runs-on: ${{ matrix.os }}
6565
steps:
6666
- name: Checkout project
67-
uses: actions/checkout@master
67+
uses: actions/checkout@v4
6868

6969
- name: Select Xcode
7070
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
@@ -77,4 +77,4 @@ jobs:
7777
7878
- name: Run tests (Swift)
7979
if: matrix.platform == 'macOS'
80-
run: swift test
80+
run: swift test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ xcuserdata/
55
DerivedData/
66
.swiftpm/
77
.netrc
8+
.claude/

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CLAUDE.md

CLAUDE.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Repository Guidelines
2+
3+
This file is operating guidance for agents and contributors working in `PersistentKeyValueKit`. Keep changes small, explicit, and aligned with the package’s public API guarantees.
4+
5+
> [!IMPORTANT]
6+
> Use relevant Agent Skills before changing Swift APIs, concurrency code, tests, or technical prose. They capture platform rules and local preferences that code search does not reliably reveal.
7+
8+
## Overview
9+
10+
`PersistentKeyValueKit` is a Swift Package Manager library for type-safe persistence on Apple platforms. It wraps `UserDefaults` and `NSUbiquitousKeyValueStore`, provides SwiftUI property wrappers, and exposes async observation for persistent key changes.
11+
12+
- **Swift tools version**: 6.2
13+
- **Platforms**: `Package.swift` is the source of truth.
14+
- **Product**: `PersistentKeyValueKit`
15+
- **Tests**: XCTest
16+
- **Runtime resources**: none
17+
18+
## Core Rules
19+
20+
Keep call sites typed. Consumers define a key once, then use that key to read, write, bind, or observe values. Store internals stay inside the package.
21+
22+
Treat public API changes as release decisions. Before adding requirements, changing defaults, renaming symbols, or raising platform minimums, identify the compatibility impact and document the reason.
23+
24+
Do not leak Foundation storage quirks into user-facing APIs. Handle normalization, launch-argument coercion, observation filtering, cancellation, and deregistration inside the package.
25+
26+
## Repository Structure
27+
28+
```text
29+
├── Package.swift
30+
├── README.md
31+
├── Sources/PersistentKeyValueKit/
32+
│ ├── Async Sequence/
33+
│ ├── Key-Value Persistible/
34+
│ ├── Persistent Key/
35+
│ ├── Persistent Key-Value Representation/
36+
│ ├── Persistent Key-Value Store/
37+
│ └── Property Wrapper/
38+
└── Tests/PersistentKeyValueKitTests/
39+
├── Scaffolding/
40+
└── Tests/
41+
```
42+
43+
Source folders are grouped by feature area. Put new code beside the feature it extends. Keep reusable mocks, observable stores, and custom persistible types in `Tests/PersistentKeyValueKitTests/Scaffolding/`.
44+
45+
## Architecture
46+
47+
`PersistentKeyProtocol` models typed keys. `PersistentKeyValueStore` defines storage operations. `KeyValuePersistible` and `PersistentKeyValueRepresentation` define conversion between Swift values and property-list-compatible storage.
48+
49+
Concrete stores stay thin:
50+
51+
- `UserDefaults`: Foundation defaults plus KVO observation.
52+
- `NSUbiquitousKeyValueStore`: iCloud key-value storage plus notification observation.
53+
- `InMemoryPersistentKeyValueStore`: test and ephemeral storage.
54+
55+
Keep `PersistentValue` and `PersistentKeyValues` behavior aligned. SwiftUI observation and async observation should agree on defaults, emitted values, unrelated-key filtering, cancellation, and deregistration unless the difference is intentional and documented.
56+
57+
## Development Commands
58+
59+
Run from the repository root.
60+
61+
```sh
62+
swift build
63+
swift test
64+
swift test --filter PersistentKeyValuesTests
65+
swift test -c release
66+
swift test -Xswiftc -strict-concurrency=complete -Xswiftc -warn-concurrency -Xswiftc -enable-actor-data-race-checks
67+
```
68+
69+
Use the filtered command while iterating. Use strict-concurrency tests for observation, locking, cancellation, or sendability changes. Use release tests for performance-sensitive changes.
70+
71+
## Programming
72+
73+
Preserve existing Swift style: 4-space indentation, grouped declarations, and `// MARK:` sections. Public types use `UpperCamelCase`; properties, functions, and tests use `lowerCamelCase`.
74+
75+
Use the static accessor pattern for reusable keys:
76+
77+
```swift
78+
extension PersistentKeyProtocol where Self == PersistentKey<Bool> {
79+
static var isFeatureEnabled: Self {
80+
Self("IsFeatureEnabled", defaultValue: false)
81+
}
82+
}
83+
```
84+
85+
Check availability before using new Apple APIs. Support every platform minimum in `Package.swift`, or add guarded fallbacks.
86+
87+
Do not add `Sendable` requirements to public value protocols unless the compatibility cost is intentional. Protect shared mutable state explicitly; Foundation store integrations should not force consumers into actor isolation.
88+
89+
Use comments for nonobvious whys only. Public documentation comments should describe behavioral contracts, especially observation lifetime, buffering, storage conversion, and compatibility constraints.
90+
91+
## Testing
92+
93+
Add focused XCTest coverage for every behavior change. Use unique keys such as `key:\(#function)` to avoid cross-test state leakage.
94+
95+
For store behavior, test each affected store. For async observation, cover initial emission, change-only streams, buffering, unrelated keys, cancellation, iterator deinitialization, deregistration, and concurrent cancellation/change races.
96+
97+
## Contributing
98+
99+
Recent commits use short imperative subjects: `Fix UserDefaults launch argument parsing`, `Improve hot path performance`. Match that style and never credit tools or agents.
100+
101+
Before committing, run:
102+
103+
```sh
104+
git log --oneline -10
105+
git status --short
106+
```
107+
108+
Pull requests must list behavior changes, test commands run, and any public API, platform minimum, concurrency, or versioning impact.
109+
110+
## Prose
111+
112+
Read surrounding prose before editing `README.md`, `AGENTS.md`, or long doc comments. Integrate changes into the document’s structure; do not append isolated notes. Tighten wording by default.
113+
114+
Watch for **phantom rationale**: prose that invents a reason for an API shape instead of stating the fact. If an API has two equivalent spellings, say that. Do not explain it with a fake workflow preference.
115+
116+
## Agent-Specific Instructions
117+
118+
Do not overwrite unrelated local changes. Commit when asked. Do not push unless explicitly requested.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ let package = Package(
66
name: "PersistentKeyValueKit",
77
platforms: [
88
.iOS(.v15),
9-
.macOS(.v12),
9+
.macOS(.v13),
1010
.tvOS(.v15),
1111
.visionOS(.v1),
1212
.watchOS(.v8),

README.md

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ PersistentKeyValueKit is backed by a robust test suite.
3333
- [x] Persistence for any type that conforms to `KeyValuePersistible`.
3434
- [x] Universal interface for `UserDefaults` and `NSUbiquitousKeyValueStore`.
3535
- [x] Type-safe property wrapper and view modifier for SwiftUI.
36+
- [x] AsyncSequence for observing key changes in any context.
3637
- [x] Built-in support for all primitive (i.e. property list) types.
3738
- [x] Built-in representations for all common ways to persist values.
3839
- [x] Keys that are only mutable in Debug builds.
@@ -45,11 +46,10 @@ PersistentKeyValueKit is backed by a robust test suite.
4546
- tvOS 15.0+
4647
- visionOS 1.0+
4748
- watchOS 8.0+
48-
- `NSUbiquitousKeyValueStore` requires watchOS 9.0+.
4949

5050
## Requirements
5151

52-
- Xcode 16.0+
52+
- Xcode 26.0+
5353

5454
## Documentation
5555

@@ -61,7 +61,7 @@ PersistentKeyValueKit is backed by a robust test suite.
6161

6262
```swift
6363
dependencies: [
64-
.package(url: "https://github.com/kylehughes/PersistentKeyValueKit.git", .upToNextMajor(from: "1.0.0")),
64+
.package(url: "https://github.com/kylehughes/PersistentKeyValueKit.git", .upToNextMajor(from: "1.1.0")),
6565
]
6666
```
6767

@@ -118,6 +118,14 @@ userDefaults.get(.runtimeColorScheme)
118118
userDefaults.set(.runtimeColorScheme, to: .dark)
119119
```
120120

121+
Observe the same key from async code.
122+
123+
```swift
124+
for await runtimeColorScheme in userDefaults.values(for: .runtimeColorScheme) {
125+
apply(runtimeColorScheme)
126+
}
127+
```
128+
121129
## Usage
122130

123131
### Keys
@@ -415,6 +423,51 @@ extension App: SwiftUI.App {
415423
}
416424
```
417425

426+
### `AsyncSequence`
427+
428+
`PersistentKeyValues` observes a persistent key as an `AsyncSequence`. Use it outside SwiftUI when you want key changes
429+
without writing KVO or `NotificationCenter` code.
430+
431+
The same sequence is available from the store.
432+
433+
```swift
434+
for await username in UserDefaults.standard.values(for: .username) {
435+
usernameLabel.text = username
436+
}
437+
```
438+
439+
It is also available from the key.
440+
441+
```swift
442+
let usernameKey: PersistentKey<String> = .username
443+
444+
for await username in usernameKey.values(in: UserDefaults.standard) {
445+
usernameLabel.text = username
446+
}
447+
```
448+
449+
By default, `values(for:)` and `values(in:)` emit the current value first, then later changes.
450+
451+
Use `changes(for:)` or `changes(in:)` to skip the current value and observe only later changes.
452+
453+
```swift
454+
for await username in UserDefaults.standard.changes(for: .username) {
455+
handleChange(username: username)
456+
}
457+
```
458+
459+
Pending changes use `.bufferingNewest(1)` by default. If the consumer falls behind, it receives the latest pending
460+
value instead of an unbounded backlog of intermediate values. Pass `.unbounded` to receive every observed value.
461+
462+
```swift
463+
for await username in UserDefaults.standard.changes(for: .username, bufferingPolicy: .unbounded) {
464+
recordChange(username: username)
465+
}
466+
```
467+
468+
Each iterator registers with the store. It deregisters when iteration ends, the iterator is released, or the task is
469+
cancelled. Iterate from a cancellable task and cancel it when the owner no longer needs updates.
470+
418471
### `UserDefaults` Registration
419472

420473
PersistentKeyValueKit supports traditional `UserDefaults` registration. The default value of the key will be registered
@@ -512,9 +565,10 @@ affordances for property list safety or proxy representations—but it is availa
512565

513566
There is no platform support for observing changes to keys in `NSUbiquitousKeyValueStore`. The only affordance is
514567
listening for external changes from other devices. PersistentKeyValueKit implements observability for all mutations
515-
made through the framework: any `@PersistentValue` using `NSUbiquitousKeyValueStore` will automatically update with any
516-
changes made by PersistentKeyValueKit anywhere, on any device. However, any changes to `NSUbiquitousKeyValueStore` made
517-
outside of the framework will not be automatically reflected in `@PersistentValue` properties.
568+
made through the framework: any `@PersistentValue` or `AsyncSequence` using `NSUbiquitousKeyValueStore` will
569+
automatically update with any changes made by PersistentKeyValueKit anywhere, on any device. However, any changes to
570+
`NSUbiquitousKeyValueStore` made outside of the framework will not be automatically reflected in `@PersistentValue`
571+
properties or `AsyncSequence` iterations.
518572

519573
## Contributions
520574

0 commit comments

Comments
 (0)