arraysswiftfor-loopindexpathipados

Remove elements from string array while in a for loop whose looping condition depends on string array size


I have a string array which I want to remove elements from depending on values in an indexPath array, generated from a 2 finger pan gesture over table rows. The loop repetition count depends on the size of the string array, and the relevant string array element is removed on each loop.

Code below:

if let indexPaths = languagesTable.indexPathsForSelectedRows {
    rowCount = rowCount - (indexPaths.count)     
    controllerData.deleteWordsForLanguage(languages: languages, indexPaths: indexPaths)        
    for indexPath in indexPaths {
        print("inside 1st for loop in clearSelectedLanguages, indexPaths are: \(indexPaths)")
        print("languages.count in clearSelectedLanguages in languagesView os: \(languages.count)")
        for language in 0..<languages.count 
            print("indexPath.row == language is: \(indexPath.row == language) && indexPath.section == 0 is: \(indexPath.section == 0)")
            if indexPath.row == language && indexPath.section == 0 {
                print("inside 2nd for loop in clearSelectedLanguages in languages")
                print("languages in clearSelectedLanguages before languages.remove in languagesView are: \(languages)")
                languages.remove(at: language)
                print("languages in clearSelectedLanguages in languagesView are: \(languages)")
                }
            }
        }
    }
    controllerData.saveLanguages(languagesToSave: languages)
    languagesTable.setEditing(false, animated: true)
    languagesTable.reloadData()
    enableSort()
    enableDelete()
}

When I ask the app to remove (pressing delete button) all elements of string array, it deletes all but one of the elements. I've tried arranging the string array elements in different orders, and it doesn't appear to be string element value specific, just always the middle element, if it was a 3 element array.

I'm suspicious that removing elements from an array, who's original size was defining the number of loops needed, is causing problems with the 2nd for loop.

Some debug console output:

inside 1st for loop in clearSelectedLanguages, indexPaths are: [[0, 0], [0, 1], [0, 2]]
indexPath.row == language is: true && indexPath.section == 0 is: true
inside 2nd for loop in clearSelectedLanguages in languages
languages in clearSelectedLanguages in languagesView are: ["french", "spanish", "german"]
languages in clearSelectedLanguages in languagesView are: ["spanish", "german"]
indexPath.row == language is: false && indexPath.section == 0 is: true
indexPath.row == language is: false && indexPath.section == 0 is: true
inside 1st for loop in clearSelectedLanguages, indexPaths are: [[0, 0], [0, 1], [0, 2]]
indexPath.row == language is: false && indexPath.section == 0 is: true
indexPath.row == language is: true && indexPath.section == 0 is: true
inside 2nd for loop in clearSelectedLanguages in languages
languages in clearSelectedLanguages in languagesView are: ["spanish", "german"]
languages in clearSelectedLanguages in languagesView are: ["spanish"]
inside 1st for loop in clearSelectedLanguages, indexPaths are: [[0, 0], [0, 1], [0, 2]]
indexPath.row == language is: false && indexPath.section == 0 is: true
languages in saveLanguages in dataModel are: ["spanish"]

As you can see, the second set of if conditions fail, and the last element of the string array never gets removed. The program then exits all for loops and calls a method that includes the final debug print statement.


Solution

  • Using a loop to remove elements by indices would often confuses us because the target index will change while executing loop.

    With indexPaths given as [[0, 0], [0, 1], [0, 2]], and languages as ["french", "spanish", "german"],

    When removing the first indexPath [0, 0], it may be OK:

    languages in clearSelectedLanguages in languagesView are: ["french", "spanish", "german"]
    languages in clearSelectedLanguages in languagesView are: ["spanish", "german"]
    

    it removes the corresponding element "french".

    But when removing the second indexPath [0, 1], you may need to check the output carefully.

    languages in clearSelectedLanguages in languagesView are: ["spanish", "german"]
    languages in clearSelectedLanguages in languagesView are: ["spanish"]
    

    It removes the (originally) third element "german" and not the second element "spanish". Because once an element is removed, the indices of each element in the Array would change.

    So, when you want remove the third indexPath [0, 2], nothing happens, as a single element Array ["spanish"] has no element of index 2.


    One way to handle this situation is to remove in reversed order, bottom to top.

        for indexPath in indexPaths.reversed() {
            //...
        }
    

    But double-loop is not an efficient way and I would write it as follows:

            if let indexPaths = languagesTable.indexPathsForSelectedRows {
                rowCount = rowCount - (indexPaths.count)
                controllerData.deleteWordsForLanguage(languages: languages, indexPaths: indexPaths)
    
                print("indexPaths are: \(indexPaths)")
                let rowsToRemove = Set(indexPaths.filter{$0.section == 0}.map{$0.row})
                print("languages before filter are: \(languages)")
                languages = languages.enumerated().filter{row,_ in !rowsToRemove.contains(row)}.map{$1}
                print("languages after filter are: \(languages)")
    
                controllerData.saveLanguages(languagesToSave: languages)
                //...
            }
    

    Please try it.