Skip to content

Instantly share code, notes, and snippets.

@appfrosch
Created July 24, 2024 06:25
Show Gist options
  • Save appfrosch/3b36c89c84d28cd792efa8b0b3c0b615 to your computer and use it in GitHub Desktop.
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)
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
}
}
@appfrosch
Copy link
Author

The issue is that the .sheet-modifier must not be within any lazy view component (like a List or a Form).
So, moving the .sheet one level down beneath the Form in this case totally fixes my problem here!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment