Skip to content

Commit d7e42c7

Browse files
[Building Sync-ups Tutorial] fixes in "Persisting sync-ups" chapter (pointfreeco#3577)
* Update SyncUpsList tutorial to use file storage with thread-safe mutations and make the tutorial app compiles correctly: - Added thread-safe mutation of shared `syncUps` using `withLock` - Updated preview to work with shared storage * Update Persisting SyncUps tests tutorial to add step to fix shared state related compilation errors - Moved `syncUps` initialization outside of `TestStore` - Added `withLock` for thread-safe shared state mutations - Updated tutorial explanation to add details about those fixes * wip --------- Co-authored-by: Stephen Celis <stephen@stephencelis.com>
1 parent 2e3e496 commit d7e42c7

11 files changed

+336
-33
lines changed

Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps-01-code-0004.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ struct SyncUpsList {
66
@ObservableState
77
struct State: Equatable {
88
@Presents var addSyncUp: SyncUpForm.State?
9-
@Shared(.syncUps) var syncUps
9+
@Shared(.fileStorage(.syncUps)) var syncUps: IdentifiedArrayOf<SyncUp> = []
1010
}
1111
enum Action {
1212
case addSyncUpButtonTapped
@@ -30,15 +30,15 @@ struct SyncUpsList {
3030
guard let newSyncUp = state.addSyncUp?.syncUp
3131
else { return .none }
3232
state.addSyncUp = nil
33-
state.syncUps.append(newSyncUp)
33+
state.$syncUps.withLock { _ = $0.append(syncUp) }
3434
return .none
3535

3636
case .discardButtonTapped:
3737
state.addSyncUp = nil
3838
return .none
3939

4040
case let .onDelete(indexSet):
41-
state.syncUps.remove(atOffsets: indexSet)
41+
state.$syncUps.withLock { $0.remove(atOffsets: indexSet) }
4242
return .none
4343

4444
case .syncUpTapped:
@@ -51,8 +51,6 @@ struct SyncUpsList {
5151
}
5252
}
5353

54-
extension SharedKey where Self == FileStorageKey<IdentifiedArrayOf<SyncUp>>.Default {
55-
static var syncUps: Self {
56-
Self[.fileStorage(.documentsDirectory.appending(component: "sync-ups.json")), default: []]
57-
}
54+
extension URL {
55+
static let syncUps = Self.documentsDirectory.appending(component: "sync-ups.json")
5856
}
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,58 @@
11
import ComposableArchitecture
22
import SwiftUI
33

4-
@main
5-
struct SyncUpsApp: App {
6-
@MainActor
7-
static let store = Store(initialState: SyncUpsList.State()) {
8-
SyncUpsList()
4+
@Reducer
5+
struct SyncUpsList {
6+
@ObservableState
7+
struct State: Equatable {
8+
@Presents var addSyncUp: SyncUpForm.State?
9+
@Shared(.syncUps) var syncUps
910
}
11+
enum Action {
12+
case addSyncUpButtonTapped
13+
case addSyncUp(PresentationAction<SyncUpForm.Action>)
14+
case confirmAddButtonTapped
15+
case discardButtonTapped
16+
case onDelete(IndexSet)
17+
case syncUpTapped(id: SyncUp.ID)
18+
}
19+
var body: some ReducerOf<Self> {
20+
Reduce { state, action in
21+
switch action {
22+
case .addSyncUpButtonTapped:
23+
state.addSyncUp = SyncUpForm.State(syncUp: SyncUp(id: SyncUp.ID()))
24+
return .none
25+
26+
case .addSyncUp:
27+
return .none
28+
29+
case .confirmAddButtonTapped:
30+
guard let newSyncUp = state.addSyncUp?.syncUp
31+
else { return .none }
32+
state.addSyncUp = nil
33+
state.$syncUps.withLock { _ = $0.append(syncUp) }
34+
return .none
35+
36+
case .discardButtonTapped:
37+
state.addSyncUp = nil
38+
return .none
39+
40+
case let .onDelete(indexSet):
41+
state.$syncUps.withLock { $0.remove(atOffsets: indexSet) }
42+
return .none
1043

11-
var body: some Scene {
12-
WindowGroup {
13-
NavigationStack {
14-
SyncUpsListView(store: Self.store)
44+
case .syncUpTapped:
45+
return .none
1546
}
1647
}
48+
.ifLet(\.$addSyncUp, action: \.addSyncUp) {
49+
SyncUpForm()
50+
}
51+
}
52+
}
53+
54+
extension SharedKey where Self == FileStorageKey<IdentifiedArrayOf<SyncUp>>.Default {
55+
static var syncUps: Self {
56+
Self[.fileStorage(.documentsDirectory.appending(component: "sync-ups.json")), default: []]
1757
}
1858
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
@Reducer
5+
struct SyncUpsList {
6+
// ...
7+
}
8+
9+
struct SyncUpsListView: View {
10+
// ...
11+
}
12+
13+
#Preview {
14+
NavigationStack {
15+
SyncUpsListView(
16+
store: Store(
17+
initialState: SyncUpsList.State(
18+
syncUps: [.mock]
19+
)
20+
) {
21+
SyncUpsList()
22+
._printChanges()
23+
}
24+
)
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
@Reducer
5+
struct SyncUpsList {
6+
// ...
7+
}
8+
9+
struct SyncUpsListView: View {
10+
// ...
11+
}
12+
13+
#Preview {
14+
@Shared(.syncUps) var syncUps = [.mock]
15+
NavigationStack {
16+
SyncUpsListView(
17+
store: Store(
18+
initialState: SyncUpsList.State()
19+
) {
20+
SyncUpsList()
21+
._printChanges()
22+
}
23+
)
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
@main
5+
struct SyncUpsApp: App {
6+
@MainActor
7+
static let store = Store(initialState: SyncUpsList.State()) {
8+
SyncUpsList()
9+
}
10+
11+
var body: some Scene {
12+
WindowGroup {
13+
NavigationStack {
14+
SyncUpsListView(store: Self.store)
15+
}
16+
}
17+
}
18+
}

Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps-01-video-0006.mov renamed to Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps-01-video-0008.mov

File renamed without changes.

Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps-02-code-0001-previous.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ struct SyncUpsListTests {
3939

4040
@Test
4141
func deletion() async {
42-
// ...
42+
let store = TestStore(
43+
initialState: SyncUpsList.State(
44+
syncUps: [
45+
SyncUp(
46+
id: SyncUp.ID(),
47+
title: "Point-Free Morning Sync"
48+
)
49+
]
50+
)
51+
) {
52+
SyncUpsList()
53+
}
54+
55+
await store.send(.onDelete([0])) {
56+
$0.syncUps = []
57+
}
4358
}
4459
}

Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps-02-code-0001.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,26 @@ struct SyncUpsListTests {
3333

3434
await store.send(.confirmAddButtonTapped) {
3535
$0.addSyncUp = nil
36-
// $0.syncUps = [editedSyncUp]
36+
$0.syncUps = [editedSyncUp]
3737
}
3838
}
3939

4040
@Test
4141
func deletion() async {
42-
// ...
42+
@Shared(.syncUps) var syncUps = [
43+
SyncUp(
44+
id: SyncUp.ID(),
45+
title: "Point-Free Morning Sync"
46+
)
47+
]
48+
let store = TestStore(
49+
initialState: SyncUpsList.State()
50+
) {
51+
SyncUpsList()
52+
}
53+
54+
await store.send(.onDelete([0])) {
55+
$0.syncUps = []
56+
}
4357
}
4458
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import ComposableArchitecture
2+
import Testing
3+
4+
@testable import SyncUps
5+
6+
@MainActor
7+
struct SyncUpsListTests {
8+
@Test
9+
func addSyncUp() async {
10+
let store = TestStore(initialState: SyncUpsList.State()) {
11+
SyncUpsList()
12+
} withDependencies: {
13+
$0.uuid = .incrementing
14+
}
15+
16+
await store.send(.addSyncUpButtonTapped) {
17+
$0.addSyncUp = SyncUpForm.State(
18+
syncUp: SyncUp(id: SyncUp.ID(0))
19+
)
20+
}
21+
22+
let editedSyncUp = SyncUp(
23+
id: SyncUp.ID(0),
24+
attendees: [
25+
Attendee(id: Attendee.ID(), name: "Blob"),
26+
Attendee(id: Attendee.ID(), name: "Blob Jr."),
27+
],
28+
title: "Point-Free morning sync"
29+
)
30+
await store.send(.addSyncUp(.presented(.set(\.syncUp, editedSyncUp)))) {
31+
$0.addSyncUp?.syncUp = editedSyncUp
32+
}
33+
34+
await store.send(.confirmAddButtonTapped) {
35+
$0.addSyncUp = nil
36+
$0.$syncUps.withLock { $0 = [editedSyncUp] }
37+
}
38+
}
39+
40+
@Test
41+
func deletion() async {
42+
@Shared(.syncUps) var syncUps = [
43+
SyncUp(
44+
id: SyncUp.ID(),
45+
title: "Point-Free Morning Sync"
46+
)
47+
]
48+
let store = TestStore(
49+
initialState: SyncUpsList.State()
50+
) {
51+
SyncUpsList()
52+
}
53+
54+
await store.send(.onDelete([0])) {
55+
$0.$syncUps.withLock { $0 = [] }
56+
}
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import ComposableArchitecture
2+
import Testing
3+
4+
@testable import SyncUps
5+
6+
@MainActor
7+
struct SyncUpsListTests {
8+
@Test
9+
func addSyncUp() async {
10+
let store = TestStore(initialState: SyncUpsList.State()) {
11+
SyncUpsList()
12+
} withDependencies: {
13+
$0.uuid = .incrementing
14+
}
15+
16+
await store.send(.addSyncUpButtonTapped) {
17+
$0.addSyncUp = SyncUpForm.State(
18+
syncUp: SyncUp(id: SyncUp.ID(0))
19+
)
20+
}
21+
22+
let editedSyncUp = SyncUp(
23+
id: SyncUp.ID(0),
24+
attendees: [
25+
Attendee(id: Attendee.ID(), name: "Blob"),
26+
Attendee(id: Attendee.ID(), name: "Blob Jr."),
27+
],
28+
title: "Point-Free morning sync"
29+
)
30+
await store.send(.addSyncUp(.presented(.set(\.syncUp, editedSyncUp)))) {
31+
$0.addSyncUp?.syncUp = editedSyncUp
32+
}
33+
34+
await store.send(.confirmAddButtonTapped) {
35+
$0.addSyncUp = nil
36+
// $0.$syncUps.withLock { $0 = [editedSyncUp] }
37+
}
38+
}
39+
40+
@Test
41+
func deletion() async {
42+
@Shared(.syncUps) var syncUps = [
43+
SyncUp(
44+
id: SyncUp.ID(),
45+
title: "Point-Free Morning Sync"
46+
)
47+
]
48+
let store = TestStore(
49+
initialState: SyncUpsList.State()
50+
) {
51+
SyncUpsList()
52+
}
53+
54+
await store.send(.onDelete([0])) {
55+
$0.$syncUps.withLock { $0 = [] }
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)