Created
July 24, 2024 06:25
-
-
Save appfrosch/3b36c89c84d28cd792efa8b0b3c0b615 to your computer and use it in GitHub Desktop.
Simplified project showing the issue of a sheet being dismissed upon first drill down (tree-navigation) while a detail screen is shown (stack-navigation)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ComposableArchitecture | |
import SwiftUI | |
@main | |
struct ItemBrainApp: App { | |
var body: some Scene { | |
WindowGroup { | |
RootFeatureView( | |
store: Store( | |
initialState: RootFeature.State( | |
path: .init(), | |
items: Item.mockArray | |
), | |
reducer: { | |
RootFeature() | |
} | |
) | |
) | |
} | |
} | |
} | |
//MARK: - FeatureLayer | |
//MARK: Root Feature | |
@Reducer | |
struct RootFeature { | |
@ObservableState | |
struct State { | |
var path: StackState<Path.State> | |
var items: [Item] | |
} | |
enum Action { | |
case path(StackAction<Path.State, Path.Action>) | |
} | |
var body: some ReducerOf<Self> { | |
Reduce { state, action in | |
switch action { | |
case .path: | |
return .none | |
} | |
} | |
.forEach(\.path, action: \.path) { | |
Path() | |
} | |
} | |
} | |
extension RootFeature { | |
@Reducer | |
struct Path: Equatable { | |
@ObservableState | |
enum State { | |
case itemDetail(ItemDetailFeature.State) | |
case subItemDetail(SubItemDetailFeature.State) | |
} | |
enum Action { | |
case itemDetail(ItemDetailFeature.Action) | |
case subItemDetail(SubItemDetailFeature.Action) | |
} | |
var body: some ReducerOf<Self> { | |
Scope(state: \.itemDetail, action: \.itemDetail) { | |
ItemDetailFeature() | |
} | |
Scope(state: \.subItemDetail, action: \.subItemDetail) { | |
SubItemDetailFeature() | |
} | |
} | |
} | |
} | |
//MARK: Item Detail Feature | |
@Reducer | |
struct ItemDetailFeature { | |
@ObservableState | |
struct State { | |
@Presents var destination: Destination.State? | |
var item: Item | |
} | |
enum Action { | |
case destination(PresentationAction<Destination.Action>) | |
case addSubItemButtonTapped | |
} | |
@Reducer | |
enum Destination { | |
case addSubItem(SubItemDetailFeature) | |
} | |
var body: some ReducerOf<Self> { | |
Reduce { | |
state, | |
action in | |
switch action { | |
case .destination: | |
return .none | |
case .addSubItemButtonTapped: | |
state.destination = .addSubItem( | |
SubItemDetailFeature.State( | |
subItem: SubItem() | |
) | |
) | |
return .none | |
} | |
} | |
.ifLet(\.$destination, action: \.destination) | |
} | |
} | |
//MARK: SubItem Detail Feature | |
@Reducer | |
struct SubItemDetailFeature { | |
@ObservableState | |
struct State { | |
var subItem: SubItem | |
init( | |
subItem: SubItem = SubItem() | |
) { | |
self.subItem = subItem | |
} | |
} | |
enum Action: BindableAction { | |
case binding(BindingAction<State>) | |
} | |
var body: some ReducerOf<Self> { | |
BindingReducer() | |
Reduce { state, action in | |
switch action { | |
case .binding: | |
return .none | |
} | |
} | |
} | |
} | |
//MARK: - View Layer | |
struct RootFeatureView: View { | |
@State var store: StoreOf<RootFeature> | |
var body: some View { | |
NavigationStack( | |
path: $store.scope( | |
state: \.path, | |
action: \.path | |
)) { | |
List { | |
ForEach(store.items) { item in | |
NavigationLink( | |
state: RootFeature.Path.State.itemDetail( | |
ItemDetailFeature.State( | |
item: item | |
) | |
)) { | |
Text(item.title) | |
} | |
} | |
} | |
} destination: { store in | |
switch store.state { | |
case .itemDetail: | |
if let store = store.scope( | |
state: \.itemDetail, | |
action: \.itemDetail | |
) { | |
ItemDetailView(store: store) | |
} | |
case .subItemDetail: | |
if let store = store.scope( | |
state: \.subItemDetail, | |
action: \.subItemDetail | |
) { | |
SubItemDetailView(store: store) | |
} | |
} | |
} | |
} | |
} | |
struct ItemDetailView: View { | |
@State var store: StoreOf<ItemDetailFeature> | |
var body: some View { | |
Form { | |
Section { | |
Text(store.item.title) | |
} | |
Section { | |
List { | |
if store.item.subItems.isEmpty { | |
Text("No subitems yet") | |
} | |
ForEach(store.item.subItems) { subItem in | |
Text(subItem.title) | |
} | |
} | |
} header: { | |
HStack { | |
Spacer() | |
Button("Add subitem", systemImage: "plus") { | |
store.send(.addSubItemButtonTapped) | |
} | |
.labelStyle(.iconOnly) | |
} | |
} | |
.sheet( | |
item: $store.scope( | |
state: \.destination?.addSubItem, | |
action: \.destination.addSubItem | |
) | |
) { store in | |
SubItemDetailView(store: store) | |
} | |
} | |
} | |
} | |
//MARK: SubItem View | |
struct SubItemDetailView: View { | |
@State var store: StoreOf<SubItemDetailFeature> | |
var body: some View { | |
Form { | |
TextField("Title", text: $store.subItem.title) | |
} | |
} | |
} | |
//MARK: - Model layer | |
struct Item: Identifiable { | |
let id: UUID | |
var title: String | |
var subItems: [SubItem] | |
init( | |
id: UUID = UUID(), | |
title: String, | |
subItems: [SubItem] = [] | |
) { | |
self.id = id | |
self.title = title | |
self.subItems = subItems | |
} | |
} | |
extension Item { | |
static let mockArray = [ | |
Item(title: "First item") | |
] | |
} | |
struct SubItem: Equatable, Identifiable { | |
let id: UUID | |
var title: String | |
init( | |
id: UUID = UUID(), | |
title: String = "" | |
) { | |
self.id = id | |
self.title = title | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The issue is that the
.sheet
-modifier must not be within any lazy view component (like aList
or aForm
).So, moving the
.sheet
one level down beneath theForm
in this case totally fixes my problem here!