Search code examples
swiftrecursionmultidimensional-array

How can I get the path to an item in a nested array in order to modify the array?


I am using Core Data with CloudKit and what I am trying to do is export the data from there into a file. Any item can have child items which means child items can have child items which means an item can be any number of levels nested. My problem is I don't know how to get the path to a nested item so that I can add its children to the itemEntries array. My other problem is that even if I did have a variable named pathToItem with the correct path such as [0][0][0] I don't know how to use that variable to update itemEntries array, as I assume itemEntries[pathToItem] wouldn't work.


    @State private var itemEntries: [ItemEntry] = []
    
    struct ItemEntry: Codable {
        var title: String
        var children: [ItemEntry]
    }
    
    private func populateNestedData(items: [Item], itemsAreNested: Bool = false) {
        for item in items {
            if (!itemsAreNested) {
                itemEntries.append(createItemEntry(item: item))
            }
            else {
              //Here is my problem. The item could be any number of levels nested. How can I get the path to the item so that I can use it to append the child item to the itemEntries array?
                let pathToItem = ?????
              //And even if I did have pathToItem such as [0][0][0] how would I then use that correctly here?
                itemEntries[pathToItem].children.append(createItemEntry(item: item))
            }
            
            if (!item.childrenArray.isEmpty) {
                var children = coreDataController.getChildrenOfItemForExporting(item: item)
                populateNestedData(items: children, itemsAreNested: true)
            }
        }
    }
    
    private func createItemEntry(item: Item) -> ItemEntry {
        return ItemEntry(
            title: item.title ?? "",
            children: []
        )
    }

Solution

  • If I understand correctly, you are looking for key paths. They represent a "path" to get/set a property of a type, or an index into an array.

    The key path will represent the array that you are appending to. You keep appending to it in each recursive call, to "go one level deeper". Initially, it is \.self, representing an "empty" path.

    private func populateNestedData(items: [Item], keyPath: WritableKeyPath<[ItemEntry], [ItemEntry]> = \.self) {
        for item in items {
            // itemEntries[keyPath: \.self] is the same as itemEntries
            // so you can get rid of the itemsAreNested parameter
            itemEntries[keyPath: keyPath].append(createItemEntry(item: item))
            
            if (!item.childrenArray.isEmpty) {
                let children = ...
                // this is the index of the last item inserted into itemEntries[keyPath: keyPath]
                let lastInsertedIndex = itemEntries[keyPath: keyPath].count - 1
                populateNestedData(
                    items: children,
                    // appends the key path representing the children of the item last inserted to itemEntries[keyPath: keyPath]
                    keyPath: keyPath.appending(path: \.[lastInsertedIndex].children)
                )
            }
        }
    }
    

    That said, I think it is better to write this as a function that returns [ItemEntry]:

    private func populateNestedData(items: [Item]) -> [ItemEntry] {
        var entries = [ItemEntry]()
        for item in items {
            var itemEntry = createItemEntry(item: item)
            
            if (!item.childrenArray.isEmpty) {
                let children = ...
                itemEntry.children = populateNestedData(items: children)
            }
            entries.append(itemEntry)
        }
        return entries
    }
    

    And assign to the itemEntries state when you want to call this:

    itemEntries = populateNestedData(items: ...)