Editable hierarchical list in runtime

Gents, dev(il)s, I am looking for a piece of code or principal explanation to realise following:

  • I have a array of struct Item{}
  • Each item has child [Item]
  • in the content view I would like to have a hierarchical displayed tree of my Items and for each Item line I would like to have a button to remove it or add a child item for a selected one

I tired List entity, navigation and have no real success. Is anybody there to give me a small guide? Thank you M

Here is a very simple example, just to show how to use hierarchical List

struct FileItem: Identifiable {
  let name: String
  var children: [FileItem]? 
  var id: String { name }
}

struct ContentView: View {
  @State var data: [FileItem] = [FileItem(name: "First", children: [FileItem(name: "child1"), FileItem(name: "child2")])] // State, so that you can modify

  var body: some View {
    List(data, children: \.children, rowContent: { Text($0.name) })
  }
}

 

In Xcode documentation (searching for List), you will find more details on how to use hierarchical lists:

Creating hierarchical lists

You can also create a hierarchical list of arbitrary depth by providing tree-structured data and a children parameter that provides a key path to get the child nodes at any level. The following example uses a deeply-nested collection of a custom FileItem type to simulate the contents of a file system. The list created from this data uses collapsing cells to allow the user to navigate the tree structure.

struct ContentView: View {
    struct FileItem: Hashable, Identifiable, CustomStringConvertible {
        var id: Self { self }
        var name: String
        var children: [FileItem]? = nil
        var description: String {
            switch children {
            case nil:
                return "📄 \(name)"
            case .some(let children):
                return children.isEmpty ? "📂 \(name)" : "📁 \(name)"
            }
        }
    }
    let fileHierarchyData: [FileItem] = [
      FileItem(name: "users", children:
        [FileItem(name: "user1234", children:
          [FileItem(name: "Photos", children:
            [FileItem(name: "photo001.jpg"),
             FileItem(name: "photo002.jpg")]),
           FileItem(name: "Movies", children:
             [FileItem(name: "movie001.mp4")]),
              FileItem(name: "Documents", children: [])
          ]),
         FileItem(name: "newuser", children:
           [FileItem(name: "Documents", children: [])
           ])
        ]),
        FileItem(name: "private", children: nil)
    ]
    var body: some View {
        List(fileHierarchyData, children: \.children) { item in
            Text(item.description)
        }
    }
}

   Here is a simple remove implementation:

struct ContentView: View {
    @State var data: [FileItem] = [FileItem(name: "First", children: [FileItem(name: "child1"), FileItem(name: "child2")])]

    var body: some View {
        List(data, children: \.children) { item in
            HStack {
                Text(item.name)
                // If there are children, we cannot remove it
                if item.children == nil || item.children!.isEmpty {
                    Spacer()
                    Button("Remove"){
                        // This is a simple implementation if only children, no grandChildren
                        // if grandchildren, need to have a recursive search for the parent
                        for (index, parent) in data.enumerated() {
                            // If it is children
                            if parent.children != nil && !parent.children!.isEmpty {
                                for child in parent.children! {
                                    if child.name == item.name {
                                        var newChildren = parent.children!
                                        newChildren.removeAll(where: { $0.name == item.name })
                                        data[index].children = newChildren
                                        print("remove \(item.name)")
                                    }
                                }
                            } else {
                                // It is the parent
                                for (index, parent) in data.enumerated() {
                                    data.remove(at: index)
                                    print("remove \(item.name)")
                                }
                            }
                        }
                    }
                }
            }
        }
    }

}

Don't forget to close the thread if that's OK. Otherwise, explain where the problem is.

Removing parent needs an additional test:

                        } else {
                            // It is the parent
                            for (index, parent) in data.enumerated() {
                                if parent.name == item.name {
                                    data.remove(at: index)
                                    print("remove \(item.name)")
                                }
                            }

For completeness, a very basic Add child. I have assumed that children in nil for a child and not nil but may be empty for parent.:

struct FileItem: Identifiable {
  let name: String
  var children: [FileItem]? // 👈🏻 Will be nil if child at lowest level of hierarchy ; otherwise for parent, should be [] when no child
  var id: String { name }
}

struct ContentView: View {
    @State var data: [FileItem] = [
        FileItem(name: "First", children: [FileItem(name: "childF1"), FileItem(name: "childF2")]),
        FileItem(name: "Second", children: [FileItem(name: "childS1"), FileItem(name: "childS2"), FileItem(name: "childS3")]),
        FileItem(name: "Third", children: [FileItem(name: "childT1"), FileItem(name: "childT2")]),
    ]

//  var body: some View {
//      List(data, children: \.children, rowContent: { Text($0.name) })
//  }
    var body: some View {
        List(data, children: \.children) { item in
            HStack {
                Text(item.name)
                // If there are no children, we cannot remove it
                if item.children != nil {   // So it is parent, maybe with no child []
                    Spacer()
                    Button("Add child") {
                        // let's search its position in data
                        for (index, parent) in data.enumerated() {
                            if parent.name == item.name && parent.children != nil { // double check on nil
                                data[index].children!.append(FileItem(name: "new child"))
                            }
                        }

                    }
                }
                if item.children == nil || item.children!.isEmpty { // nil when item is child, empty for parent
                    Spacer()
                    Button("Remove") {
                        // This is a simple implementation if only children, no grandChildren
                        // if grandchildren, need to have a recursive search for the parent
                        for (index, parent) in data.enumerated() {
                            // If it is children
                            if parent.children != nil && !parent.children!.isEmpty {
                                for child in parent.children! {
                                    if child.name == item.name {
                                        var newChildren = parent.children!
                                        newChildren.removeAll(where: { $0.name == item.name })
                                        data[index].children = newChildren
                                        print("remove \(item.name)")
                                    }
                                }
                            } else {
                                // It is the parent
                                for (index, parent) in data.enumerated() {
                                    if parent.name == item.name {
                                        data.remove(at: index)
                                        print("remove \(item.name)")
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

}

Hello

this is not really what is want to achieve. When you click on the list line/item, you actually click on the complete "line". In the code you provided, you can have for each item only one function, Add or Remove. I need to have both for each line item. Purpose is to create deeper hierarchy. Something like virtual nodes/folders and populate these with files or other struct{} types Do I need to use another type of swift entity to display my list instead of List?

When you add on child line, what do you add ? A child of the parent ? A child of the child ?

 

I need to have both for each line item.

So simple! Remove some tests.

Here is an updated code…

struct ContentView: View {
    
    @State var data: [FileItem] = [
        FileItem(name: "First", children: [FileItem(name: "childF1"), FileItem(name: "childF2")]),
        FileItem(name: "Second", children: [FileItem(name: "childS1"), FileItem(name: "childS2"), FileItem(name: "childS3")]),
        FileItem(name: "Third", children: [FileItem(name: "childT1"), FileItem(name: "childT2")]),
    ]

    @FocusState private var focusedField: String?

    var body: some View {
        List($data, children: \.children) { $item in
            HStack {
                TextField("", text: $item.name) 
                    .focused($focusedField, equals: item.name)  // avoid keyboard to disappear at each character
                    .task {
                      self.focusedField = item.name
                    }
//                Text(item.name)
                // If there are no children, we cannot remove it
                if item.children != nil {   // So it is parent, maybe with no child []
                    Spacer()
                    Button("Add child") {
                        // let's search its position in data
                        print("added")
                        for (index, parent) in data.enumerated() {
                            if parent.name == item.name && parent.children != nil { // double check on nil
                                data[index].children!.append(FileItem(name: "new child"))
                            }
                        }

                    }
                    .buttonStyle(.borderless)
                }
//                if item.children == nil || item.children!.isEmpty { // nil when item is child, empty for parent
                    Spacer()
                    Button("Remove") {
                        // This is a simple implementation if only children, no grandChildren
                        // if grandchildren, need to have a recursive search for the parent
                        var deleteDone = false
                        for (index, individual) in data.enumerated() {
                            // remove child
                            if !deleteDone {
                                if individual.children != nil && !individual.children!.isEmpty {
                                    for child in individual.children! {
                                        if child.name == item.name {
                                            var newChildren = individual.children!
                                            newChildren.removeAll(where: { $0.name == item.name })
                                            data[index].children = newChildren
                                            deleteDone = true
                                            break
                                        }
                                    }
                                }
                            } // REMOVE THIS -> else {
                                // remove parent
                            if !deleteDone {
                                for (index, individual) in data.enumerated() {
                                    if individual.name == item.name {
                                        data.remove(at: index)
                                        deleteDone = true
                                        break
                                    }
                                }
                            }
//                            }
                        }
                    }
                    .buttonStyle(.borderless)
//                }
            }
        }
    }

}

There are still some tuning to be done (error when removing an added child). But that should give you a start point.

As I explained, if you want more levels, you can use the same struct and List. But you will have to write the function to search for an item in the hierarchy in order to add or remove it in the data. That's a good exercise.

I was puzzled by the crash when removing an added item.

That had to do with the focus on the TextField…

This works more properly:

struct FileItem: Identifiable {
  var name: String
  var children: [FileItem]? // 👈🏻 Will be nil if child at lowest level of hierarchy ; otherwise for parent, should be [] when no child
  var id: String { name }
}

struct ContentView: View {

    @State var data: [FileItem] = [
        FileItem(name: "First", children: [FileItem(name: "childF1"), FileItem(name: "childF2")]),
        FileItem(name: "Second", children: [FileItem(name: "childS1"), FileItem(name: "childS2"), FileItem(name: "childS3")]),
        FileItem(name: "Third", children: [FileItem(name: "childT1"), FileItem(name: "childT2")])
    ]

    @FocusState private var focusedField: String?

    var body: some View {
        List($data, children: \.children) { $item in
            HStack {
                TextField("", text: $item.name) // probleme : le clavier disparait à chaque caractère
                // Avec focused, on passe au dernier TextField
                    .focused($focusedField, equals: item.name)
                    .task {
                      self.focusedField = item.name
                    }

                // If there are no children, we cannot remove it
                if item.children != nil {   // So it is parent, maybe with no child []
                    Spacer()
                    Button("Add child") {
                        // let's search its position in data
                        for (index, parent) in data.enumerated() {
                            if parent.name == item.name && parent.children != nil { // double check on nil
                                let childrenCount = parent.children!.count
                                var newChildName = "new child \(childrenCount+1)"
                                for child in parent.children! { // let's not give the same name twice
                                    if newChildName == child.name {
                                        newChildName = "new child \(childrenCount+100)"
                                    }
                                }
                                data[index].children!.append(FileItem(name: newChildName))
                            }
                        }

                    }
                    .foregroundColor(.green)
                    .buttonStyle(.borderless)
                }

                Spacer()

                Button("Remove") {
                    // This is a simple implementation if only children, no grandChildren
                    // if grandchildren, need to have a recursive search for the parent
                    self.focusedField = nil
                    var deleteDone = false
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // <<-- To give time to update focus
                        for (index, individual) in data.enumerated() {
                            // remove child
                            if deleteDone { print("I quit loop") ; break }
                            if !deleteDone {
                                if individual.children != nil && !individual.children!.isEmpty {
                                    for child in individual.children! {
                                        if child.name == item.name {
                                            var newChildren = individual.children!
                                            newChildren.removeAll(where: { $0.name == item.name })
                                            data[index].children = newChildren
                                            deleteDone = true
                                            break
                                        }
                                    }
                                }

                            // remove parent
                            if !deleteDone {
                                for (index, individual) in data.enumerated() {
                                    if individual.name == item.name {
                                        data.remove(at: index)
                                        deleteDone = true
                                        break
                                    }
                                }
                            }
                        }
                    }
                }
                .foregroundColor(.red)
                .buttonStyle(.borderless)
            }
        }
    }

}
Editable hierarchical list in runtime
 
 
Q