Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save appfrosch/a477f720772dcc95924ba96a116380ab to your computer and use it in GitHub Desktop.
Save appfrosch/a477f720772dcc95924ba96a116380ab to your computer and use it in GitHub Desktop.
TCA StackNavigation w/ updates from child to parent
import ComposableArchitecture
import SwiftUI
@main
struct so_tca_StackNavigationUpdateParentFromChildApp: App {
var body: some Scene {
WindowGroup {
ParentView(
store: Store(
initialState: ParentFeature.State(),
reducer: { ParentFeature() })
)
}
}
}
struct Item: Equatable, Identifiable {
var id: UUID
var text: String
init(
id: UUID = UUID(),
text: String = "New item"
) {
self.id = id
self.text = text
}
}
@Reducer
struct ParentFeature {
@ObservableState
struct State {
var items: [Item]
var path = StackState<Path.State>()
init(
items: [Item] = [
Item(text: "Item A"),
Item(text: "Item B"),
Item(text: "Item C")
]
) {
self.items = items
}
}
enum Action {
case path(StackAction<Path.State, Path.Action>)
}
@Reducer
struct Path {
@ObservableState
enum State {
case itemDetail(ChildFeature.State)
}
enum Action {
case itemDetail(ChildFeature.Action)
}
var body: some ReducerOf<Self> {
Scope(state: /State.itemDetail, action: /Action.itemDetail) {
ChildFeature()
}
}
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case.path(.element(id: _, action: .itemDetail(.delegate(let delegateAction)))):
switch delegateAction {
case .cancelButtonTapped:
return .none
case .saveButtonTapped(let mutatedItem):
if let index = state.items.firstIndex(where: { $0.id == mutatedItem.id }) {
state.items[index] = mutatedItem
}
return .none
}
case .path:
return .none
}
}
.forEach(\.path, action: /Action.path) {
Path()
}
}
}
struct ParentView: View {
@State var store: StoreOf<ParentFeature>
var body: some View {
NavigationStack(
path: $store.scope(state: \.path, action: \.path)
) {
List {
ForEach(store.items) { item in
NavigationLink(
state: ParentFeature.Path.State.itemDetail(ChildFeature.State(item: item))) {
Text(item.text)
}
}
}
} destination: { store in
switch store.state {
case .itemDetail:
if let store = store.scope(
state: \.itemDetail,
action: \.itemDetail
) {
ChildView(store: store)
}
}
}
}
}
@Reducer
struct ChildFeature {
@ObservableState
struct State {
var item: Item
var mutatingItem: Item
var hasNoChanges: Bool {
item == mutatingItem
}
init(
item: Item
) {
self.item = item
self.mutatingItem = item
}
}
enum Action: BindableAction {
case binding(BindingAction<State>)
case delegate(Delegate)
enum Delegate {
case cancelButtonTapped
case saveButtonTapped(Item)
}
}
@Dependency(\.dismiss) var dismiss
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
return .none
case .delegate:
return .run { _ in
await self.dismiss()
}
}
}
}
}
struct ChildView: View {
@State var store: StoreOf<ChildFeature>
var body: some View {
Form {
TextField("Item", text: $store.mutatingItem.text)
}
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", role: .cancel) {
store.send(.delegate(.cancelButtonTapped))
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
store.send(.delegate(.saveButtonTapped(store.mutatingItem)))
}
.disabled(store.hasNoChanges)
}
}
}
}
#Preview {
ParentView(
store: Store(
initialState: ParentFeature.State(),
reducer: { ParentFeature() })
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment