How to filter one array based on another array?

I have 2 arrays. The 1st, "data", is filled with text that goes in a tableview. The 2nd, "identities", is filled with storyboard ids. Both array indexes match to one another, as in if the user chooses the 1st item in the data array, then they will segue to the 1st storyboard id in the identities array.


The problem is, when the 1st array is sorted, the second array stays the same. So the storyboard id no longer matches the cell of the tableview.


I would like to somehow either align the indexes, so that they both react to the filter in the same way, or possibly change the 2 arrays into 1. But the way I segue needs to stay close to the same.


Here is the code:


    @IBOutlet weak var toolTable: UITableView!
    @IBOutlet weak var searchForTool: UISearchBar!
    var searchActive : Bool = false
    var filtered : [String] = []
    var data = ["  Data1", "  Data2"]
    var identities = ["A", "B"]
   
    override func viewDidLoad() {
        super.viewDidLoad()
       
        toolTable.delegate = self
        toolTable.dataSource = self
       
        searchForTool.delegate = self
       
    }
   
    func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
        searchActive = true
    }
   
    func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
        searchActive = false
    }
   
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        searchActive = false
    }
   
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        searchActive = false
    }
   
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
       
       
        filtered = data.filter { text in
           
            return text.lowercased().contains(searchText.lowercased())
       
        }
       
        if(filtered.count == 0){
            searchActive = false
        } else {
            searchActive = true
        }
        self.toolTable.reloadData()
    }
    override func numberOfSections(in tableView: UITableView) -> Int {
       
        return 1
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      
        if(searchActive) {
            return filtered.count
        }
        return data.count
    }
   
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
       
        if(searchActive){
            cell.toolLabel.text = filtered[indexPath.row]
        } else {
            cell.toolLabel.text = data[indexPath.row]
        }
       
        return cell
    }
   
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let vcName = identities[indexPath.row]
        let viewController = storyboard?.instantiateViewController(withIdentifier: vcName)
        self.navigationController?.pushViewController(viewController!, animated: true)
       
    }
   
}



Any help would be greatly appreciated.

Answered by OOPer in 256728022

change the 2 arrays into 1

This seems to be the preferred way.


    //...
    //### change the 2 arrays into 1
    typealias Item = (data: String, identity: String)
//    var filtered : [String] = []
//    var data = ["  Data1", "  Data2"]
//    var identities = ["A", "B"]
    var filtered: [Item] = []
    var items: [Item] = [
        (data: "  Data1", identity: "A"),
        (data: "  Data2", identity: "B")
    ]

    //...

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        filtered = items.filter { item in
            item.data.localizedCaseInsensitiveContains(searchText)
        }
   
        searchActive = !filtered.isEmpty
        self.toolTable.reloadData()
    }

    //...

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if searchActive {
            return filtered.count
        }
        return items.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
   
        if searchActive {
            cell.toolLabel.text = filtered[indexPath.row].data
        } else {
            cell.toolLabel.text = items[indexPath.row].data
        }
   
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let vcName = items[indexPath.row].identity
        let viewController = storyboard?.instantiateViewController(withIdentifier: vcName)
        self.navigationController?.pushViewController(viewController!, animated: true)
    }


This is a simplified example, so the type `Item` is a tuple type. You can make it to a struct or a class.

Not too far from your original code, I believe.

Accepted Answer

change the 2 arrays into 1

This seems to be the preferred way.


    //...
    //### change the 2 arrays into 1
    typealias Item = (data: String, identity: String)
//    var filtered : [String] = []
//    var data = ["  Data1", "  Data2"]
//    var identities = ["A", "B"]
    var filtered: [Item] = []
    var items: [Item] = [
        (data: "  Data1", identity: "A"),
        (data: "  Data2", identity: "B")
    ]

    //...

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        filtered = items.filter { item in
            item.data.localizedCaseInsensitiveContains(searchText)
        }
   
        searchActive = !filtered.isEmpty
        self.toolTable.reloadData()
    }

    //...

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if searchActive {
            return filtered.count
        }
        return items.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
   
        if searchActive {
            cell.toolLabel.text = filtered[indexPath.row].data
        } else {
            cell.toolLabel.text = items[indexPath.row].data
        }
   
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let vcName = items[indexPath.row].identity
        let viewController = storyboard?.instantiateViewController(withIdentifier: vcName)
        self.navigationController?.pushViewController(viewController!, animated: true)
    }


This is a simplified example, so the type `Item` is a tuple type. You can make it to a struct or a class.

Not too far from your original code, I believe.

I've implemented your code, but it seems to work the same as before.


e.g: When you type "Data2" in the search bar, it filteres properly to only show "Data2", but when pressed, it still segues to "Data1"s page, which is identity "A", not "B"..


A bit strange

A bit strange

It's not strange, sorry. I've forgotten to include `searchActive`-testing into the method `tableView(_:didSelectRowAt:)`:


    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let vcName: String
        if searchActive {
            vcName = filtered[indexPath.row].identity
        } else {
            vcName = items[indexPath.row].identity
        }
        let viewController = storyboard?.instantiateViewController(withIdentifier: vcName)
        self.navigationController?.pushViewController(viewController!, animated: true)
    }


Or, you can use a little trick:

    //...
    var searchActive : Bool = false
    typealias Item = (data: String, identity: String)
    var filtered: [Item] = []
    var items: [Item] = [
        (data: "  Data1", identity: "A"),
        (data: "  Data2", identity: "B")
    ]
    //### a little trick...
    var activeItems: [Item] {
        if searchActive {
            return filtered
        } else {
            return items
        }
    }

    //...  
  
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        filtered = items.filter { item in
            item.data.localizedCaseInsensitiveContains(searchText)
        }
      
        searchActive = !filtered.isEmpty
        self.toolTable.reloadData()
    }

    //...

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return activeItems.count
    }
  
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
      
        cell.toolLabel.text = activeItems[indexPath.row].data
      
        return cell
    }
  
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let vcName = activeItems[indexPath.row].identity
        let viewController = storyboard?.instantiateViewController(withIdentifier: vcName)
        self.navigationController?.pushViewController(viewController!, animated: true)
    }

Works almost perfectly! The only problem left, is if I click on the search bar (without typing anything in), then click on data1 or data2, it gives the "Index out of range" error.

I see. That may happen. Changing the value of `searchActive` is changing the contents of your TableView, so it should be reloaded.


Please try this:

    var searchActive : Bool = false {
        didSet {
            if searchActive != oldValue {
                toolTable?.reloadData()
            }
        }
    }

Correct me if I'm wrong, but I tried plugging that code in under..


filtered = items.filter { item in
   item.data.localizedCaseInsensitiveContains(searchText)
}


while getting rid of..


searchActive = !filtered.isEmpty


otherwise it gives me an error.


Either way, its still giving me the error, and not reseting my tableview when the search is cleared.


-Sorry I'm still a novice at iOS dev

In Swift, `var ...` is a declaration which introduces a new property or a local variable, so if it already exists, we usually assume it as replacing the existing declaration. But, of course, I should have noted it.


Replace the existing property declaration:

    var searchActive : Bool = false

To the declaration including `didSet`:

    var searchActive : Bool = false {
        didSet {
            if searchActive != oldValue {
                toolTable?.reloadData()
            }
        }
    }

I probably should have been able to figure that out. But I did implement your code. The only issue left, is when the search bar is first clicked, the tableview is cleared, and the 'x' (cancel button) is missing on the search bar.


Afterword the it seems to work fine.

the 'x' (cancel button) is missing on the search bar

You can add one line to your `viewDidLoad()`:

    override func viewDidLoad() {
        super.viewDidLoad()
   
        toolTable.delegate = self
        toolTable.dataSource = self
   
        searchForTool.delegate = self
        searchForTool.showsCancelButton = true //<-
    }

(Or you can set it in the storyboard editor.)


when the search bar is first clicked, the tableview is cleared

It is a problem of app design. Your code (including my fixes) shows `filtered` content when `searchActive` is `true`. Always gets cleared is one possible design. If you think it is not an intended behavior, you need to design when and where you should set `searchActive` to `true`.

Ah ok. Well thanks for the help and patience, I should be able to fix the last bits. I'll mark your first responce as the correct answer, and the rest as helpful.

How to filter one array based on another array?
 
 
Q