@@ -33,6 +33,7 @@ system, such as SQLite.
33
33
* [ Observing changes to shared state] ( #Observing-changes-to-shared-state )
34
34
* [ Initialization rules] ( #Initialization-rules )
35
35
* [ Deriving shared state] ( #Deriving-shared-state )
36
+ * [ Concurrent mutations to shared state] ( #Concurrent-mutations-to-shared-state )
36
37
* [ Testing shared state] ( #Testing-shared-state )
37
38
* [ Testing when using persistence] ( #Testing-when-using-persistence )
38
39
* [ Testing when using custom persistence strategies] ( #Testing-when-using-custom-persistence-strategies )
@@ -41,7 +42,6 @@ system, such as SQLite.
41
42
* [ Testing tips] ( #Testing-tips )
42
43
* [ Read-only shared state] ( #Read-only-shared-state )
43
44
* [ Type-safe keys] ( #Type-safe-keys )
44
- * [ Concurrent mutations to shared state] ( #Concurrent-mutations-to-shared-state )
45
45
* [ Shared state in pre-observation apps] ( #Shared-state-in-pre-observation-apps )
46
46
* [ Gotchas of @Shared ] ( #Gotchas-of-Shared )
47
47
@@ -446,6 +446,39 @@ else { return }
446
446
todo // Shared<Todo>
447
447
```
448
448
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
+
449
482
## Testing shared state
450
483
451
484
Shared state behaves quite a bit different from the regular state held in Composable Architecture
@@ -476,15 +509,15 @@ struct Feature {
476
509
Reduce { state, action in
477
510
switch action {
478
511
case .incrementButtonTapped :
479
- state.count += 1
512
+ state.$ count. withLock { $0 += 1 }
480
513
return .none
481
514
}
482
515
}
483
516
}
484
517
}
485
518
```
486
519
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:
488
521
489
522
```swift
490
523
@Test
@@ -494,7 +527,7 @@ func increment() async {
494
527
}
495
528
496
529
await store.send (.incrementButtonTapped ) {
497
- $0 .count = 1
530
+ $0 .$ count. withLock { $0 = 1 }
498
531
}
499
532
}
500
533
```
@@ -511,7 +544,7 @@ func increment() async {
511
544
}
512
545
513
546
await store.send (.incrementButtonTapped ) {
514
- $0 .count = 2
547
+ $0 .$ count. withLock { $0 = 2 }
515
548
}
516
549
}
517
550
```
@@ -590,7 +623,7 @@ func increment() async {
590
623
}
591
624
await store.send (.incrementButtonTapped )
592
625
store.assert {
593
- $0 .count = 1
626
+ $0 .$ count. withLock { $0 = 1 }
594
627
}
595
628
}
596
629
```
@@ -964,36 +997,6 @@ struct FeatureView: View {
964
997
}
965
998
```
966
999
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
-
997
1000
## Gotchas of @Shared
998
1001
999
1002
There are a few gotchas to be aware of when using shared state in the Composable Architecture.
0 commit comments