Swift UItableview with Search bar and checkmark

3.5k views Asked by At

I've been trying to do SearchBar on UITableView with checkmark. Where user can checkmark only one at a time. I managed to do checkmark and search. But when I search and checkmark on the search result, that is where it started to fail

import UIKit

class CandyTableViewController : UITableViewController, UISearchBarDelegate, UISearchDisplayDelegate {

    var lastSelectedIndexPath = NSIndexPath(forRow: 0, inSection: 0)

    var candies = [Candy]()
    var mySelected=String()


    var filteredCandies = [Candy]()

    override func viewDidLoad() {
        self.candies = [Candy(category:"Chocolate", name:"chocolate Bar"),
            Candy(category:"Chocolate", name:"chocolate Chip"),
            Candy(category:"Chocolate", name:"dark chocolate"),
            Candy(category:"Hard", name:"lollipop"),
            Candy(category:"Hard", name:"candy cane"),
            Candy(category:"Hard", name:"jaw breaker"),
            Candy(category:"Other", name:"caramel"),
            Candy(category:"Other", name:"sour chew"),
            Candy(category:"Other", name:"gummi bear")]

        mySelected=candies[lastSelectedIndexPath.row].name

    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if tableView == self.searchDisplayController!.searchResultsTableView {
            return self.filteredCandies.count
        } else {
            return self.candies.count
        }
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell") as UITableViewCell

        cell.accessoryType = .Checkmark


        var candy : Candy
        if tableView == self.searchDisplayController!.searchResultsTableView {
            candy = filteredCandies[indexPath.row]

        } else {
            candy = candies[indexPath.row]

        }

        if mySelected==candy.name {
            cell.accessoryType = .Checkmark
            lastSelectedIndexPath=indexPath
        } else {
            cell.accessoryType = .None
        }

        cell.textLabel.text = candy.name

        return cell
    }

    func filterContentForSearchText(searchText: String, scope: String = "All") {
        self.filteredCandies = self.candies.filter({( candy : Candy) -> Bool in
            var categoryMatch = (scope == "All") || (candy.category == scope)
            var stringMatch = candy.name.rangeOfString(searchText)
            return categoryMatch && (stringMatch != nil)
            })
    }

    func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchString searchString: String!) -> Bool {
        let scopes = self.searchDisplayController!.searchBar.scopeButtonTitles as [String]
        let selectedScope = scopes[self.searchDisplayController!.searchBar.selectedScopeButtonIndex] as String
        self.filterContentForSearchText(searchString, scope: selectedScope)
        return true
    }

    func searchDisplayController(controller: UISearchDisplayController!,
        shouldReloadTableForSearchScope searchOption: Int) -> Bool {
            let scope = self.searchDisplayController!.searchBar.scopeButtonTitles as [String]
            self.filterContentForSearchText(self.searchDisplayController!.searchBar.text, scope: scope[searchOption])
            return true
    }

    func searchDisplayController(controller: UISearchDisplayController, willHideSearchResultsTableView tableView: UITableView) {
        self.tableView.reloadData()
    }

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

        tableView.deselectRowAtIndexPath(indexPath, animated: true)

       if indexPath.row != lastSelectedIndexPath?.row {
            if let lastSelectedIndexPath = lastSelectedIndexPath {
                let oldCell = tableView.cellForRowAtIndexPath(lastSelectedIndexPath)
                oldCell?.accessoryType = .None
            }
            let newCell = tableView.cellForRowAtIndexPath(indexPath)
            newCell?.accessoryType = .Checkmark
            lastSelectedIndexPath = indexPath
            mySelected = newCell?.textLabel.text ?? ""


      }

        if candies[indexPath.row].name != candies[lastSelectedIndexPath.row].name {

                let oldCell = tableView.cellForRowAtIndexPath(lastSelectedIndexPath)
                oldCell?.accessoryType = .None
            }

            let newCell = tableView.cellForRowAtIndexPath(indexPath)
            newCell?.accessoryType = .Checkmark

            lastSelectedIndexPath = indexPath

            mySelected = newCell?.textLabel.text ?? ""


    }

  }

I want it to be able to checkmark after search. And keep it that way when return back to full list. Please help, I've been tried to solve this for few days already.

Update: I managed to do as intended. But believe the codes can be more simpler than this

2

There are 2 answers

3
Nate Cook On BEST ANSWER

You have a bug to fix, then a couple changes to make the selection from the search results show up in the regular table view.

First, the bug - in tableView:didSelectRowAtIndexPath:, you're setting myselected like this:

myselected=candies[indexPath.row].name

That works fine in the regular table view, but in the search results, indexPath.row isn't pointing at the right member of the candies array. Since you're keeping track by the name of the candy, just grab the name back out of the selected row:

myselected = newCell?.textLabel?.text ?? ""

If you run your code with this change, you won't see any difference, because you aren't reloading the main table view after the search. To do that, implement the delegate method searchDisplayController:willHideSearchResultsTableView::

func searchDisplayController(controller: UISearchDisplayController, willHideSearchResultsTableView tableView: UITableView) {
    self.tableView.reloadData()
}

Almost there! When configuring your cells for the table view, you're adding a checkmark to the current cell, but aren't clearing it from the others. Change your code slightly to this:

if myselected==candy.name {
    cell.accessoryType = .Checkmark
    lastSelectedIndexPath=indexPath
} else {
    cell.accessoryType = .None
}
0
Vasily On

Solution is a bit tricky. One of the problems was fixed by @Nate Cook:

mySelected = newCell?.textLabel.text ?? ""

The second problem is deselecting correctly in search mode and normal mode. You need to understand that NSIndexPath is dynamic value on your situation. E.g.:

Table in normal mode (index = indexPath.row)

+-------+------+
| Index | Name |
+-------+------+
|     0 | A    |
|     1 | B    |
|     2 | C    |
|     3 | BB   |
+-------+------+

For example we have selected row 4, so now we have lastSelectedIndexPath = 3. If you select new row, you code will correctly deselect row with lastSelectedIndexPath.

But… Let's look what's going on when you activated search with keyword B.

+-------+------+
| Index | Name |
+-------+------+
|     0 | B    |
|     1 | BB   |
+-------+------+

Look in indexes, they changed! Now when you're trying to select row with name B your script tries to deselect row with lastIndexPath = 3. But there is no row with such index because you have absolutely new indexes.

So what we can do? We can regenerate lastIndexPath every time when UITableView making cell with Checkmark. You have done that already, and that is correct solution, I just describe more detailed what's going on (you've changed your code without description).

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        // cutted some code
        cell.accessoryType = .Checkmark
        lastSelectedIndexPath=indexPath
}

If you have only one checkmark that code is most simple solution. If you need to store multiple checkmarks you need to write much more complex code. That question was only about storing 1 checkmark with searchController.