Meet The Composable Architecture tutorial various fixes and improvements (#3568)

* Fix tutorial documentation typo: sync ups to contacts

* Update tutorial documentation: Use .foregroundStyle for icon color to replace deprecated .foregroundColor

* Update tutorial documentation: Clarify that Reducer() is a macro

* Update tutorial documentation: Modify test code to use `.modify` for enum mutation to fix issue reported in https://github.com/pointfreeco/swift-composable-architecture/discussions/3158

* Update tutorial documentation: Add @CasePathable to Alert enum and simplify test case syntax

* Add @CasePathable to Alert enum in tutorial documentation to be consistent with previous part of the tutorial

* Update tutorial documentation: Simplify StackAction with StackActionOf typealias

* Update Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/04-NavigationStacks/02-04-NavigationStacks.tutorial

---------

Co-authored-by: Stephen Celis <stephen.celis@gmail.com>
This commit is contained in:
Frédéric Ruaudel
2025-01-29 21:11:05 +01:00
committed by GitHub
parent 1599296395
commit 3395a96fd9
21 changed files with 36 additions and 20 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@
/*.swiftinterface
/*.xcodeproj
xcuserdata/
.docc-build/

View File

@@ -258,7 +258,7 @@
> Important: Delegate actions are the most general way of communicating from the child domain
back to the parent, but there are other techniques. We could have also utilized the
`@Shared` property wrapper for the collection of sync ups, which would allow the
`@Shared` property wrapper for the collection of contacts, which would allow the
`AddContactFeature` to insert a new contact directly into the parent collection without any
further steps. This can be powerful, but we will use delegate actions for this tutorial. To
read more about `@Shared` see the <doc:SharingState> article, and see the

View File

@@ -12,7 +12,7 @@ struct ContactsView: View {
store.send(.deleteButtonTapped(id: contact.id))
} label: {
Image(systemName: "trash")
.foregroundColor(.red)
.foregroundStyle(.red)
}
}
}

View File

@@ -131,7 +131,7 @@
@Step {
Add a case for for the "Add contact" feature. Note that we are holding onto the actual
`AddContactFeature` reducer in the case, not the state. The
``ComposableArchitecture/Reducer()`` will fill in all the requirements for the reducer
``ComposableArchitecture/Reducer()`` macro will fill in all the requirements for the reducer
protocol for us automatically.
@Code(name: "ContactsFeatures.swift", file: 02-02-02-code-0001.swift)

View File

@@ -22,7 +22,7 @@ struct ContactsFeatureTests {
)
}
await store.send(\.destination.addContact.setName, "Blob Jr.") {
$0.destination?.addContact?.contact.name = "Blob Jr."
$0.destination?.modify(\.addContact) { $0.contact.name = "Blob Jr." }
}
}
}

View File

@@ -22,7 +22,7 @@ struct ContactsFeatureTests {
)
}
await store.send(\.destination.addContact.setName, "Blob Jr.") {
$0.destination?.addContact?.contact.name = "Blob Jr."
$0.destination?.modify(\.addContact) { $0.contact.name = "Blob Jr." }
}
await store.send(\.destination.addContact.saveButtonTapped)
}

View File

@@ -22,7 +22,7 @@ struct ContactsFeatureTests {
)
}
await store.send(\.destination.addContact.setName, "Blob Jr.") {
$0.destination?.addContact?.contact.name = "Blob Jr."
$0.destination?.modify(\.addContact) { $0.contact.name = "Blob Jr." }
}
await store.send(\.destination.addContact.saveButtonTapped)
await store.receive(

View File

@@ -22,7 +22,7 @@ struct ContactsFeatureTests {
)
}
await store.send(\.destination.addContact.setName, "Blob Jr.") {
$0.destination?.addContact?.contact.name = "Blob Jr."
$0.destination?.modify(\.addContact) { $0.contact.name = "Blob Jr." }
}
await store.send(\.destination.addContact.saveButtonTapped)
await store.receive(

View File

@@ -22,7 +22,7 @@ struct ContactsFeatureTests {
)
}
await store.send(\.destination.addContact.setName, "Blob Jr.") {
$0.destination?.addContact?.contact.name = "Blob Jr."
$0.destination?.modify(\.addContact) { $0.contact.name = "Blob Jr." }
}
await store.send(\.destination.addContact.saveButtonTapped)
await store.receive(

View File

@@ -22,7 +22,7 @@ struct ContactsFeatureTests {
)
}
await store.send(\.destination.addContact.setName, "Blob Jr.") {
$0.destination?.addContact?.contact.name = "Blob Jr."
$0.destination?.modify(\.addContact) { $0.contact.name = "Blob Jr." }
}
await store.send(\.destination.addContact.saveButtonTapped)
await store.receive(

View File

@@ -9,6 +9,7 @@ struct ContactsFeature {
case addButtonTapped
case deleteButtonTapped(id: Contact.ID)
case destination(PresentationAction<Destination.Action>)
@CasePathable
enum Alert: Equatable {
case confirmDeletion(id: Contact.ID)
}

View File

@@ -22,7 +22,7 @@ struct ContactsFeatureTests {
await store.send(.deleteButtonTapped(id: UUID(1))) {
$0.destination = .alert(.deleteConfirmation(id: UUID(1)))
}
await store.send(.destination(.presented(.alert(.confirmDeletion(id: UUID(1)))))) {
await store.send(\.destination.alert.confirmDeletion, UUID(1)) {
}
}
}

View File

@@ -22,8 +22,10 @@ struct ContactsFeatureTests {
await store.send(.deleteButtonTapped(id: UUID(1))) {
$0.destination = .alert(.deleteConfirmation(id: UUID(1)))
}
await store.send(.destination(.presented(.alert(.confirmDeletion(id: UUID(1)))))) {
$0.contacts.remove(id: UUID(1))
await store.send(\.destination.alert.confirmDeletion, UUID(1)) {
$0.contacts = [
Contact(id: UUID(0), name: "Blob")
]
$0.destination = nil
}
}

View File

@@ -116,9 +116,9 @@
To do this we can chain into the `addContact` case name directly and mutate a part of its
associated value.
> Tip: To chain into an enum and mutate an associated value, the enum must be annotated with
> `@CasePathable` _and_ `@dynamicMemberLookup`. The `@Reducer` macro automatically applies
> these annotations to enum-based `State`, but you must manually apply it to other enums.
> Tip: To use the `modify` helper on an enum to mutate an associated value, the enum must
> be annotated with `@CasePathable`. The `@Reducer` macro automatically applies
> this annotation to enum-based `State`, but you must manually apply it to other enums.
@Code(name: "ContactsFeatureTests.swift", file: 02-03-01-code-0011.swift)
}
@@ -146,7 +146,7 @@
}
@Step {
To further assert that when the `saveContact` delegate action was received, you must
To further assert that the `saveContact` delegate action was received, you must
annotate `AddContactFeature.Action.Delegate` with the `@CasePathable` macro.
@Code(name: "ContactsFeature.swift", file: 02-03-01-code-0015.swift, previousFile: 02-03-01-code-0015-previous.swift)
@@ -308,6 +308,9 @@
@Step {
Make use of the new `deleteConfirmation` static alert function in the `ContactsFeature`
reducer, rather than building `AlertState` from scratch.
Also to further assert that the `confirmDeletion` action was received,
annotate `ContactsFeature.Action.Alert` with the `@CasePathable` macro.
@Code(name: "ContactsFeature.swift", file: 02-03-03-code-0007.swift, previousFile: 02-03-03-code-0007-previous.swift)
}

View File

@@ -11,6 +11,7 @@ struct ContactsFeature {
case addButtonTapped
case deleteButtonTapped(id: Contact.ID)
case destination(PresentationAction<Destination.Action>)
@CasePathable
enum Alert: Equatable {
case confirmDeletion(id: Contact.ID)
}

View File

@@ -12,6 +12,7 @@ struct ContactsFeature {
case addButtonTapped
case deleteButtonTapped(id: Contact.ID)
case destination(PresentationAction<Destination.Action>)
@CasePathable
enum Alert: Equatable {
case confirmDeletion(id: Contact.ID)
}

View File

@@ -12,7 +12,8 @@ struct ContactsFeature {
case addButtonTapped
case deleteButtonTapped(id: Contact.ID)
case destination(PresentationAction<Destination.Action>)
case path(StackAction<ContactDetailFeature.State, ContactDetailFeature.Action>)
case path(StackActionOf<ContactDetailFeature>)
@CasePathable
enum Alert: Equatable {
case confirmDeletion(id: Contact.ID)
}

View File

@@ -12,7 +12,8 @@ struct ContactsFeature {
case addButtonTapped
case deleteButtonTapped(id: Contact.ID)
case destination(PresentationAction<Destination.Action>)
case path(StackAction<ContactDetailFeature.State, ContactDetailFeature.Action>)
case path(StackActionOf<ContactDetailFeature>)
@CasePathable
enum Alert: Equatable {
case confirmDeletion(id: Contact.ID)
}

View File

@@ -12,7 +12,8 @@ struct ContactsFeature {
case addButtonTapped
case deleteButtonTapped(id: Contact.ID)
case destination(PresentationAction<Destination.Action>)
case path(StackAction<ContactDetailFeature.State, ContactDetailFeature.Action>)
case path(StackActionOf<ContactDetailFeature>)
@CasePathable
enum Alert: Equatable {
case confirmDeletion(id: Contact.ID)
}

View File

@@ -12,7 +12,8 @@ struct ContactsFeature {
case addButtonTapped
case deleteButtonTapped(id: Contact.ID)
case destination(PresentationAction<Destination.Action>)
case path(StackAction<ContactDetailFeature.State, ContactDetailFeature.Action>)
case path(StackActionOf<ContactDetailFeature>)
@CasePathable
enum Alert: Equatable {
case confirmDeletion(id: Contact.ID)
}

View File

@@ -101,6 +101,9 @@
This represents the actions that can happen inside the stack, such as pushing or popping
an element off the stack, or an action happening inside a particular feature inside the
stack.
> Tip: ``StackAction`` is generic over both state and action of the `Path` domain, and so
> you can use the ``StackActionOf`` type alias to simplify the syntax a bit.
We will also handle the `.path` case in the reducer and return
``ComposableArchitecture/Effect/none`` for now.