Skip to content

Commit 47d606b

Browse files
committed
Merge remote-tracking branch 'origin/main' into core
2 parents bbe33b7 + 8ffc4df commit 47d606b

File tree

124 files changed

+896
-694
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

124 files changed

+896
-694
lines changed

.github/workflows/documentation.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ concurrency:
1717

1818
jobs:
1919
build:
20-
runs-on: macos-14
20+
runs-on: macos-15
2121
steps:
22-
- name: Select Xcode 16.0
23-
run: sudo xcode-select -s /Applications/Xcode_16.0.app
22+
- name: Select Xcode 16.2
23+
run: sudo xcode-select -s /Applications/Xcode_16.2.app
2424

2525
- name: Checkout Package
2626
uses: actions/checkout@v4

ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved

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

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ let package = Package(
2626
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.4.0"),
2727
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.1.0"),
2828
.package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0"),
29-
.package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.2.2"),
29+
.package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.3.0"),
3030
.package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"),
3131
.package(url: "https://github.com/pointfreeco/swift-sharing", "0.1.2"..<"3.0.0"),
3232
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.3.0"),

[email protected]

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ let package = Package(
2626
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.4.0"),
2727
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.1.0"),
2828
.package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0"),
29-
.package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.2.2"),
29+
.package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.3.0"),
3030
.package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"),
3131
.package(url: "https://github.com/pointfreeco/swift-sharing", "0.1.2"..<"3.0.0"),
3232
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.3.0"),

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, iPadOS, visionO
1414
* [Examples](#examples)
1515
* [Basic usage](#basic-usage)
1616
* [Documentation](#documentation)
17+
* [FAQ](#faq)
1718
* [Community](#community)
1819
* [Installation](#installation)
1920
* [Translations](#translations)
@@ -575,6 +576,11 @@ comfortable with the library:
575576
* [Concurrency][concurrency-article]
576577
* [Bindings][bindings-article]
577578

579+
## FAQ
580+
581+
We have a [dedicated article][faq-article] for all of the most frequently asked questions and
582+
comments people have concerning the library.
583+
578584
## Community
579585

580586
If you want to discuss the Composable Architecture or have a question about how to use it to solve
@@ -633,17 +639,13 @@ The following translations of this README have been contributed by members of th
633639
* [Russian](https://gist.github.com/SubvertDev/3317d0c3b35ed601be330d6fc0df5aba)
634640
* [Simplified Chinese](https://gist.github.com/sh3l6orrr/10c8f7c634a892a9c37214f3211242ad)
635641
* [Spanish](https://gist.github.com/pitt500/f5e32fccb575ce112ffea2827c7bf942)
642+
* [Turkish](https://gist.github.com/gokhanamal/93001244ef0c1cec58abeb1afc0de37c)
636643
* [Ukrainian](https://gist.github.com/barabashd/33b64676195ce41f4bb73c327ea512a8)
637644

638645
If you'd like to contribute a translation, please [open a
639646
PR](https://github.com/pointfreeco/swift-composable-architecture/edit/main/README.md) with a link
640647
to a [Gist](https://gist.github.com)!
641648

642-
## FAQ
643-
644-
We have a [dedicated article][faq-article] for all of the most frequently asked questions and
645-
comments people have concerning the library.
646-
647649
## Credits and thanks
648650

649651
The following people gave feedback on the library at its early stages and helped make the library

Sources/ComposableArchitecture/Documentation.docc/Articles/FAQ.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ However, one does not need to have any prior experience with these concepts. The
217217
[dependency-management-article]: <doc:DependencyManagement>
218218
[sharing-state-article]: <doc:SharingState>
219219
[navigation-article]: <doc:Navigation>
220-
[testing-article]: <doc:Testing>
220+
[testing-article]: <doc:TestingTCA>
221221
[migration-1.7-article]: <doc:MigratingTo1.7>
222222
[migration-1.10-article]: <doc:MigratingTo1.10>
223223
[sync-ups-tutorial]: <doc:BuildingSyncUps>

Sources/ComposableArchitecture/Documentation.docc/Articles/GettingStarted.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ doing much additional work.
252252

253253
## Testing your feature
254254

255-
> Note: For more in-depth information on testing, see the dedicated <doc:Testing>
255+
> Note: For more in-depth information on testing, see the dedicated <doc:TestingTCA>
256256
article.
257257

258258
To test use a `TestStore`, which can be created with the same information as the `Store`, but it
@@ -464,7 +464,7 @@ let store = TestStore(initialState: Feature.State()) {
464464

465465
That is the basics of building and testing a feature in the Composable Architecture. There are
466466
_a lot_ more things to be explored. Be sure to check out the <doc:MeetComposableArchitecture>
467-
tutorial, as well as dedicated articles on <doc:DependencyManagement>, <doc:Testing>,
467+
tutorial, as well as dedicated articles on <doc:DependencyManagement>, <doc:TestingTCA>,
468468
<doc:Navigation>, <doc:Performance>, and more. Also, the [Examples][examples] directory has
469469
a bunch of projects to explore to see more advanced usages.
470470

Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md

Lines changed: 95 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ system, such as SQLite.
3333
* [Observing changes to shared state](#Observing-changes-to-shared-state)
3434
* [Initialization rules](#Initialization-rules)
3535
* [Deriving shared state](#Deriving-shared-state)
36+
* [Concurrent mutations to shared state](#Concurrent-mutations-to-shared-state)
3637
* [Testing shared state](#Testing-shared-state)
3738
* [Testing when using persistence](#Testing-when-using-persistence)
3839
* [Testing when using custom persistence strategies](#Testing-when-using-custom-persistence-strategies)
@@ -41,7 +42,6 @@ system, such as SQLite.
4142
* [Testing tips](#Testing-tips)
4243
* [Read-only shared state](#Read-only-shared-state)
4344
* [Type-safe keys](#Type-safe-keys)
44-
* [Concurrent mutations to shared state](#Concurrent-mutations-to-shared-state)
4545
* [Shared state in pre-observation apps](#Shared-state-in-pre-observation-apps)
4646
* [Gotchas of @Shared](#Gotchas-of-Shared)
4747

@@ -446,13 +446,46 @@ else { return }
446446
todo // Shared<Todo>
447447
```
448448

449+
## Concurrent mutations to shared state
450+
451+
[mutating-shared-state-article]: https://swiftpackageindex.com/pointfreeco/swift-sharing/main/documentation/sharing/mutatingsharedstate
452+
453+
While the `@Shared` property wrapper makes it possible to treat shared state
454+
_mostly_ like regular state, you do have to perform some extra steps to mutate shared state.
455+
This is because shared state is technically a reference deep down, even
456+
though we take extra steps to make it appear value-like. And this means it's possible to mutate the
457+
same piece of shared state from multiple threads, and hence race conditions are possible. See
458+
[Mutating Shared State][mutating-shared-state-article] for a more in-depth explanation.
459+
460+
To mutate a piece of shared state in an isolated fashion, use the `withLock` method
461+
defined on the `@Shared` projected value:
462+
463+
```swift
464+
state.$count.withLock { $0 += 1 }
465+
```
466+
467+
That locks the entire unit of work of reading the current count, incrementing it, and storing it
468+
back in the reference.
469+
470+
Technically it is still possible to write code that has race conditions, such as this silly example:
471+
472+
```swift
473+
let currentCount = state.count
474+
state.$count.withLock { $0 = currentCount + 1 }
475+
```
476+
477+
But there is no way to 100% prevent race conditions in code. Even actors are susceptible to
478+
problems due to re-entrancy. To avoid problems like the above we recommend wrapping as many
479+
mutations of the shared state as possible in a single `withLock`. That will make
480+
sure that the full unit of work is guarded by a lock.
481+
449482
## Testing shared state
450483

451484
Shared state behaves quite a bit different from the regular state held in Composable Architecture
452485
features. It is capable of being changed by any part of the application, not just when an action is
453486
sent to the store, and it has reference semantics rather than value semantics. Typically references
454487
cause serious problems with testing, especially exhaustive testing that the library prefers (see
455-
<doc:Testing>), because references cannot be copied and so one cannot inspect the changes
488+
<doc:TestingTCA>), because references cannot be copied and so one cannot inspect the changes
456489
before and after an action is sent.
457490

458491
For this reason, the `@Shared` property wrapper does extra work during testing to preserve a
@@ -476,15 +509,15 @@ struct Feature {
476509
Reduce { state, action in
477510
switch action {
478511
case .incrementButtonTapped:
479-
state.count += 1
512+
state.$count.withLock { $0 += 1 }
480513
return .none
481514
}
482515
}
483516
}
484517
}
485518
```
486519

487-
This feature can be tested in exactly the same way as when you are using non-shared state:
520+
This feature can be tested in a similar same way as when you are using non-shared state:
488521

489522
```swift
490523
@Test
@@ -494,7 +527,7 @@ func increment() async {
494527
}
495528

496529
await store.send(.incrementButtonTapped) {
497-
$0.count = 1
530+
$0.$count.withLock { $0 = 1 }
498531
}
499532
}
500533
```
@@ -511,7 +544,7 @@ func increment() async {
511544
}
512545

513546
await store.send(.incrementButtonTapped) {
514-
$0.count = 2
547+
$0.$count.withLock { $0 = 2 }
515548
}
516549
}
517550
```
@@ -590,7 +623,7 @@ func increment() async {
590623
}
591624
await store.send(.incrementButtonTapped)
592625
store.assert {
593-
$0.count = 1
626+
$0.$count.withLock { $0 = 1 }
594627
}
595628
}
596629
```
@@ -658,11 +691,11 @@ func basics() {
658691
However, if your test suite is a part of an app target, then the entry point of the app will execute
659692
and potentially cause an early access of `@Shared`, thus capturing a different default value than
660693
what is specified above. This quirk of tests in app targets is documented in
661-
<doc:Testing#Testing-gotchas> of the <doc:Testing> article, and a similar quirk
694+
<doc:TestingTCA#Testing-gotchas> of the <doc:TestingTCA> article, and a similar quirk
662695
exists for Xcode previews and is discussed below in <doc:SharingState#Gotchas-of-Shared>.
663696

664697
The most robust workaround to this issue is to simply not execute your app's entry point when tests
665-
are running, which we detail in <doc:Testing#Testing-host-application>. This makes it so that you
698+
are running, which we detail in <doc:TestingTCA#Testing-host-application>. This makes it so that you
666699
are not accidentally execute network requests, tracking analytics, etc. while running tests.
667700

668701
You can also work around this issue by simply setting the shared state again after initializing
@@ -964,36 +997,6 @@ struct FeatureView: View {
964997
}
965998
```
966999

967-
## Concurrent mutations to shared state
968-
969-
While the [`@Shared`](<doc:Shared>) property wrapper makes it possible to treat shared state
970-
_mostly_ like regular state, you do have to perform some extra steps to mutate shared state.
971-
This is because shared state is technically a reference deep down, even
972-
though we take extra steps to make it appear value-like. And this means it's possible to mutate the
973-
same piece of shared state from multiple threads, and hence race conditions are possible.
974-
975-
To mutate a piece of shared state in an isolated fashion, use the `withLock` method
976-
defined on the `@Shared` projected value:
977-
978-
```swift
979-
state.$count.withLock { $0 += 1 }
980-
```
981-
982-
That locks the entire unit of work of reading the current count, incrementing it, and storing it
983-
back in the reference.
984-
985-
Technically it is still possible to write code that has race conditions, such as this silly example:
986-
987-
```swift
988-
let currentCount = state.count
989-
state.$count.withLock { $0 = currentCount + 1 }
990-
```
991-
992-
But there is no way to 100% prevent race conditions in code. Even actors are susceptible to
993-
problems due to re-entrancy. To avoid problems like the above we recommend wrapping as many
994-
mutations of the shared state as possible in a single `withLock`. That will make
995-
sure that the full unit of work is guarded by a lock.
996-
9971000
## Gotchas of @Shared
9981001

9991002
There are a few gotchas to be aware of when using shared state in the Composable Architecture.
@@ -1048,61 +1051,73 @@ extension AppState: Codable {
10481051
}
10491052
```
10501053

1051-
#### Previews
1054+
#### Tests
10521055

1053-
When a preview is run in an app target, the entry point is also created. This means if your entry
1054-
point looks something like this:
1056+
While shared properties are compatible with the Composable Architecture's testing tools, assertions
1057+
may not correspond directly to a particular action when several actions are received by effects.
10551058

1056-
```swift
1057-
@main
1058-
struct MainApp: App {
1059-
let store = Store()
1059+
Take this simple example, in which a `tap` action kicks off an effect that returns a `response`,
1060+
which finally mutates some shared state:
10601061

1061-
var body: some Scene {
1062-
1062+
```swift
1063+
@Reducer
1064+
struct Feature {
1065+
struct State: Equatable {
1066+
@Shared(value: false) var bool
1067+
}
1068+
enum Action {
1069+
case tap
1070+
case response
1071+
}
1072+
var body: some ReducerOf<Self> {
1073+
Reduce { state, action in
1074+
switch action {
1075+
case .tap:
1076+
return .run { send in
1077+
await send(.response)
1078+
}
1079+
case .response:
1080+
state.$bool.withLock { $0.toggle() }
1081+
return .none
1082+
}
1083+
}
10631084
}
10641085
}
10651086
```
10661087

1067-
then a store will be created each time you run your preview. This can be problematic with `@Shared`
1068-
and persistence strategies because the first access of a `@Shared` property will use the default
1069-
value provided, and that will cause `@Shared`'s created later to ignore the default. That will mean
1070-
you cannot override shared state in previews.
1071-
1072-
The fix is to delay creation of the store until the entry point's `body` is executed. Further, it
1073-
can be a good idea to also not run the `body` when in tests because that can also interfere with
1074-
tests (as documented in <doc:Testing#Testing-gotchas>). Here is one way this can be accomplished:
1088+
We would expect to assert against this mutation when the test store receives the `response` action,
1089+
but this will fail:
10751090

10761091
```swift
1077-
import ComposableArchitecture
1078-
import SwiftUI
1079-
1080-
@main
1081-
struct MainApp: App {
1082-
@MainActor
1083-
static let store = Store()
1084-
1085-
var body: some Scene {
1086-
WindowGroup {
1087-
if isTesting {
1088-
// NB: Don't run application in tests to avoid interference
1089-
// between the app and the test.
1090-
EmptyView()
1091-
} else {
1092-
AppView(store: Self.store)
1093-
}
1094-
}
1095-
}
1092+
// ❌ State was not expected to change, but a change occurred: …
1093+
//
1094+
// Feature.State(
1095+
// - _shared: #1 false
1096+
// + _shared: #1 true
1097+
//   )
1098+
//
1099+
// (Expected: −, Actual: +)
1100+
await store.send(.tap)
1101+
1102+
// ❌ Expected state to change, but no change occurred.
1103+
await store.receive(.response) {
1104+
$0.$shared.withLock { $0 = true }
10961105
}
10971106
```
10981107

1099-
Alternatively you can take an extra step to override shared state in your previews:
1108+
This is due to an implementation detail of the `TestStore` that predates `@Shared`, in which the
1109+
test store eagerly processes all actions received _before_ you have asserted on them. As such, you
1110+
must always assert against shared state mutations in the first action:
11001111

11011112
```swift
1102-
#Preview {
1103-
@Shared(.appStorage("isOn")) var isOn = true
1104-
isOn = true
1113+
await store.send(.tap) { //
1114+
$0.$shared.withLock { $0 = true }
11051115
}
1116+
1117+
// ❌ Expected state to change, but no change occurred.
1118+
await store.receive(.response) //
11061119
```
11071120

1108-
The second assignment of `isOn` will guarantee that it holds a value of `true`.
1121+
In a future major version of the Composable Architecture, we will be able to introduce a breaking
1122+
change that allows you to assert against shared state mutations in the action that performed the
1123+
mutation.

0 commit comments

Comments
 (0)