0
votes

I'm sure it's something very silly but how should one reset the state value of a child view when another state has changed?

For example, the code below shows 2 folders, which respectively have 2 and 3 items., which can be edited.

If you select the second folder (Work) and its 3rd item (Peter) and then select the first folder (Home), the app crashes since selectedItemIndex is out of bounds.

I tried to "reset" the state value when the view gets initialized but it seems like changing the state like such triggers out a "runtime: SwiftUI: Modifying state during view update, this will cause undefined behavior." warning.

init(items: Binding<[Item]>) {  
    self._items = items  
    self._selectedItemIndex = State(wrappedValue: 0)  
}  

What is the proper way to do this? Thanks!

Here's the code:

AppDelegate.swift


    import Cocoa
    import SwiftUI

    @NSApplicationMain
    class AppDelegate: NSObject, NSApplicationDelegate {

        var window: NSWindow!


        func applicationDidFinishLaunching(_ aNotification: Notification) {
            // Create the SwiftUI view that provides the window contents.
            let store = ItemStore()
            let contentView = ContentView(store: store)

            // Create the window and set the content view. 
            window = NSWindow(
                contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
                styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
                backing: .buffered, defer: false)
            window.center()
            window.setFrameAutosaveName("Main Window")
            window.contentView = NSHostingView(rootView: contentView)
            window.makeKeyAndOrderFront(nil)
        }

        func applicationWillTerminate(_ aNotification: Notification) {
            // Insert code here to tear down your application
        }
    }

ContentView.swift


    import SwiftUI

    final class ItemStore: ObservableObject {
        @Published var data: [Folder] = [Folder(name: "Home",
                                                items: [Item(name: "Mark"), Item(name: "Vincent")]),
                                         Folder(name: "Work",
                                                items:[Item(name: "Joseph"), Item(name: "Phil"), Item(name: "Peter")])]
    }

    struct Folder: Identifiable {
        var id = UUID()
        var name: String
        var items: [Item]
    }

    struct Item: Identifiable {
        static func == (lhs: Item, rhs: Item) -> Bool {
            return true
        }

        var id = UUID()
        var name: String
        var content = Date().description

        init(name: String) {
            self.name = name
        }
    }

    struct ContentView: View {
        @ObservedObject var store: ItemStore

        @State var selectedFolderIndex: Int?

        var body: some View {
            HSplitView {
                // FOLDERS
                List(selection: $selectedFolderIndex) {
                    Section(header: Text("Groups")) {
                        ForEach(store.data.indexed(), id: \.1.id) { index, folder in
                            Text(folder.name).tag(index)
                        }
                    }.collapsible(false)
                }
                .listStyle(SidebarListStyle())

                // ITEMS
                if selectedFolderIndex != nil {
                    ItemsView(items: $store.data[selectedFolderIndex!].items)
                }
            }
            .frame(minWidth: 800, maxWidth: .infinity, maxHeight: .infinity)
        }
    }


    struct ItemsView: View {
        @Binding var items: [Item]
        @State var selectedItemIndex: Int?

        var body: some View {
            HSplitView {
                List(selection: $selectedItemIndex) {
                    ForEach(items.indexed(), id: \.1.id) { index, item in
                        Text(item.name).tag(index)
                    }
                }
                .frame(width: 300)

                if selectedItemIndex != nil {
                    DetailView(item: $items[selectedItemIndex!])
                    .padding()
                    .frame(minWidth: 200, maxHeight: .infinity)
                }
            }
        }

        init(items: Binding) {
            self._items = items
            self._selectedItemIndex = State(wrappedValue: 0)
        }
    }


    struct DetailView: View {
        @Binding var item: Item

        var body: some View {
            VStack {
                TextField("", text: $item.name)
            }
        }
    }

    // Credit: https://swiftwithmajid.com/2019/07/03/managing-data-flow-in-swiftui/

    struct IndexedCollection: RandomAccessCollection {
        typealias Index = Base.Index
        typealias Element = (index: Index, element: Base.Element)

        let base: Base

        var startIndex: Index { base.startIndex }

        var endIndex: Index { base.endIndex }

        func index(after i: Index) -> Index {
            base.index(after: i)
        }

        func index(before i: Index) -> Index {
            base.index(before: i)
        }

        func index(_ i: Index, offsetBy distance: Int) -> Index {
            base.index(i, offsetBy: distance)
        }

        subscript(position: Index) -> Element {
            (index: position, element: base[position])
        }
    }

    extension RandomAccessCollection {
        func indexed() -> IndexedCollection {
            IndexedCollection(base: self)
        }
    }

2
Does this answer your question? Handling derived state in SwiftUIarsenius
I tried pasting your code into Xcode 11.4 and there quite a few compiler errors, so it's hard to experiment with it. Is there supposed to be a type parameter on IndexedCollection? Something may not have made it into what you pasted into your question. I see Binding<[Item]> became Binding for example.Rob N

2 Answers

1
votes

Fully playable sample draft for ContentView.swift. Play with it in both edit modes (inactive/active row selection) and adopt to your needs.

import SwiftUI

struct ItemStore {
    var data: [Folder] = [Folder(name: "Home", items: [Item(name: "Mark"), Item(name: "Vincent")]),
                          Folder(name: "Work", items:[Item(name: "Joseph"), Item(name: "Phil"), Item(name: "Peter")])]
}

struct Folder: Identifiable {
    var id = UUID()
    var name: String
    var items: [Item]
}

struct Item: Identifiable {

    var id = UUID()
    var name: String
    var content = Date().description

}

struct ContentView: View {
    @State var store: ItemStore

    @State var selectedFolderIndex: Int? = 0
    @State private var editMode = EditMode.inactive
    var body: some View {
        NavigationView {
            VStack {
                // FOLDERS

                List(selection: $selectedFolderIndex) {
                    Section(header: Text("Groups")) {
                        ForEach(store.data.indexed(), id: \.1.id) { index, folder in
                            HStack {
                                Text(folder.name).tag(index)
                                Spacer()
                            }
                                .background(Color.white) //make the whole row tapable, not just the text
                            .frame(maxWidth: .infinity)
                            .multilineTextAlignment(.leading)
                            .onTapGesture {
                                self.selectedFolderIndex = index
                            }
                        }.onDelete(perform: delete)
                    }
                }
                .listStyle(GroupedListStyle())
                .id(selectedFolderIndex)
                // ITEMS
                if selectedFolderIndex != nil && (($store.data.wrappedValue.startIndex..<$store.data.wrappedValue.endIndex).contains(selectedFolderIndex!) ){
                    ItemsView(items: $store.data[selectedFolderIndex!].items)
                }
            }
            .navigationBarTitle("Title")
            .navigationBarItems(trailing: EditButton())
            .environment(\.editMode, $editMode)
        }
    }

    func delete(at offsets: IndexSet) {
        $store.wrappedValue.data.remove(atOffsets: offsets) // Note projected value! `store.data.remove() will not modify SwiftUI on changes and it will crash because of invalid index.
    }
}


struct ItemsView: View {
    @Binding var items: [Item]
    @State var selectedDetailIndex: Int?
    var body: some View {
        HStack {
            List(selection: $selectedDetailIndex) {
                ForEach(items.indexed(), id: \.1.id) { index, item in
                    Text(item.name).tag(index)
                    .onTapGesture {
                        self.selectedDetailIndex = index
                    }
                }

            }

            if selectedDetailIndex != nil && (($items.wrappedValue.startIndex..<$items.wrappedValue.endIndex).contains(selectedDetailIndex!) )    {
                DetailView(item: $items[selectedDetailIndex!])
                    .padding()
            }
        }

    }

}


struct DetailView: View {
    @Binding var item: Item

    var body: some View {
        VStack {
            TextField("", text: $item.name)
        }
    }
}

// Credit: https://swiftwithmajid.com/2019/07/03/managing-data-flow-in-swiftui/

struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
    typealias Index = Base.Index
    typealias Element = (index: Index, element: Base.Element)

    let base: Base

    var startIndex: Index { base.startIndex }

    var endIndex: Index { base.endIndex }

    func index(after i: Index) -> Index {
        base.index(after: i)
    }

    func index(before i: Index) -> Index {
        base.index(before: i)
    }

    func index(_ i: Index, offsetBy distance: Int) -> Index {
        base.index(i, offsetBy: distance)
    }

    subscript(position: Index) -> Element {
        (index: position, element: base[position])
    }
}

extension RandomAccessCollection {
    func indexed() -> IndexedCollection<Self> {
        IndexedCollection(base: self)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(store: ItemStore())
    }
}
0
votes

Thanks to @jordanpittman for suggesting a fix:

ItemsView(items: $store.data[selectedFolderIndex!].items).id(selectedRowIndex)

Source: https://swiftui-lab.com/swiftui-id