0

I have a tableview which has 2 sections. Both of the sections have UISearchBar in the indexPath.row 0 and the rest of the rows in each section populate the list of array. Whenever I type some text in search bar every time the searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) delegate method gets called and inside the delegate method I call tableView.reloadData() to reload the search results in tableview.

tableview

Now the problem is each time the tableView reloads the UISearchBar reloads too (as UISearchbar is in row number 1) and every time the SearchBar keypad Resigns.

Instead of doing tableView.reloadData() I even tried to reload every row except the first one using bellow code

let allButFirst = (self.tableView.indexPathsForVisibleRows ?? []).filter { $0.section != selectedSection || $0.row != 0 }
self.tableView.reloadRows(at: allButFirst, with: .automatic)

But no luck. App gets crashed saying

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert row 2 into section 0, but there are only 2 rows in section 0 after the update'

chirag90
  • 2,211
  • 1
  • 22
  • 37

3 Answers3

1

You are probably changing the data source and then you are reloading rows at index paths what doesn't exist yet.

It is not so easy, but let's have an example: Before you start typing, the search result will contain something like this:

["aa", "ab", "ba", "bb"]

Then you will type "a" to the search bar and data source changes into: ["aa", "ab"]

tableView.deleteRows(at: [IndexPath(row:3, section: 0), IndexPath(row:4, section: 0)], with: .automatic)

then you delete everything in this searchbar and your data source will change to the default: ["aa", "ab", "ba", "bb"]

so in this case you need to call:

tableView.insertRows(at: [IndexPath(row:3, section: 0), IndexPath(row:4, section: 0)], with: .automatic) 

I created some working example - without storyboard source, I believe it is pretty simple to recreated it according this class.

class SearchCell: UITableViewCell {
    @IBOutlet weak var textField:UITextField?
}

class TextCell: UITableViewCell {
    @IBOutlet weak var label:UILabel?
}

class ViewController: UIViewController, UITableViewDataSource, UITextFieldDelegate {

    @IBOutlet weak var tableView: UITableView?
    weak var firstSectionTextField: UITextField?
    var originalDataSource:[[String]] = [["aa","ab","ba","bb"], ["aa","ab","ba","bb"]]
    var dataSource:[[String]] = []
    let skipRowWithSearchInput = 1

    override func viewDidLoad() {
        super.viewDidLoad()
        dataSource = originalDataSource
        tableView?.tableFooterView = UIView()
        tableView?.tableHeaderView = UIView()
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return dataSource.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource[section].count + skipRowWithSearchInput
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if indexPath.row == 0, let cell = tableView.dequeueReusableCell(withIdentifier: "search", for: indexPath) as? SearchCell {
            cell.textField?.removeTarget(self, action: #selector(textFieldDidChangeText(sender:)), for: .editingChanged)
            cell.textField?.addTarget(self, action: #selector(textFieldDidChangeText(sender:)), for: .editingChanged)
            if indexPath.section == 0 {
                firstSectionTextField = cell.textField
            }
            return cell
        } else if let cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? TextCell  {
            cell.label?.text = dataSource[indexPath.section][indexPath.row - skipRowWithSearchInput]
            return cell
        } else {
            return UITableViewCell()
        }
    }

    @objc func textFieldDidChangeText(sender: UITextField) {

        let section = sender == firstSectionTextField ? 0 : 1
        let text = sender.text ?? ""
        let oldDataSource:[String] = dataSource[section]
        //if the search bar is empty then use the original data source to display all results, or initial one
        let newDataSource:[String] = text.count == 0 ? originalDataSource[section] : originalDataSource[section].filter({$0.contains(text)})
        var insertedRows:[IndexPath] = []
        var deletedRows:[IndexPath] = []
        var movedRows:[(from:IndexPath,to:IndexPath)] = []

        //resolve inserted rows
        newDataSource.enumerated().forEach { (tuple) in let (toIndex, element) = tuple
            if oldDataSource.contains(element) == false {
                insertedRows.append(IndexPath(row: toIndex + skipRowWithSearchInput, section: section))                    
            }
        }

        //resolve deleted rows
        oldDataSource.enumerated().forEach { (tuple) in let (fromIndex, element) = tuple
            if newDataSource.contains(element) == false {
                deletedRows.append(IndexPath(row: fromIndex + skipRowWithSearchInput, section: section))
            }
        }

        //resolve moved rows
        oldDataSource.enumerated().forEach { (tuple) in let (index, element) = tuple
            if newDataSource.count > index, let offset = newDataSource.firstIndex(where: {element == $0}), index != offset {
                movedRows.append((from: IndexPath(row: index + skipRowWithSearchInput, section: section), to: IndexPath(row: offset + skipRowWithSearchInput, section: section)))
            }
        }

        //now set dataSource for uitableview, right before you are doing the changes
        dataSource[section] = newDataSource
        tableView?.beginUpdates()
        if insertedRows.count > 0 {
            tableView?.insertRows(at: insertedRows, with: .automatic)
        }
        if deletedRows.count > 0 {
            tableView?.deleteRows(at: deletedRows, with: .automatic)
        }
        movedRows.forEach({
            tableView?.moveRow(at: $0.from, to: $0.to)
        })

        tableView?.endUpdates()
    }
}

the result:

Reloading Specific row in table View

If do you need to clarify something, feel free to ask in comment.

Lukáš Mareda
  • 331
  • 2
  • 7
  • Thanks Lukas, I will try those ways. – Snigdharani Sahu Feb 07 '20 at 09:10
  • @SnigdharaniSahu since you add the picture of your case, I am afraid, that the easier options what I suggested will not help you. You are using the headers for sections already, right? So if you are interested I can provide you (as an update of my answer) more complex example about handling dataSource change. – Lukáš Mareda Feb 07 '20 at 09:33
  • Please can you tell me the best way ? – Snigdharani Sahu Feb 10 '20 at 09:01
  • Main problem is when i reload my table or section , the search bar get reloaded too. And the searchbar keypad resigns. How can i Prevent that ? – Snigdharani Sahu Feb 10 '20 at 09:04
  • @SnigdharaniSahu I updated the answer, hope it will help you, or somebody. – Lukáš Mareda Feb 11 '20 at 22:35
  • Lukas , Thnx, i tried with your second option and it worked only i took two sections one for search field and another for reloading data. I took separate custom cell for search and took outlet in that class itself. and in viewForHeaderInSection i returned customCell.contentView Then I called tableview ReloadData in searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) It worked without problm. – Snigdharani Sahu Feb 12 '20 at 08:30
0

Try this-

tableView.beginUpdates()
//Do the update thing
tableView.endUpdates()
0

It worked. I took two sections one for search field and another for reloading data (rows populating data). I took separate custom cell for search and took outlet in that class itself. and in viewForHeaderInSection I used tableView.dequeueReusableCell(withIdentifier:) and returned customCell.contentView Then I called tableview.ReloadData() in searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) It worked without problem.