Uitableview Filtering

UITableView filtering

You should create a "Model" for the Person (using MVC pattern):

First, create "Person" Model:

struct Person {
var name: String?
var imageName: String?
}

instead of using two separated arrays for storing the persons's data, you can create an array of Person Model:

// add those vars to your ViewController:
var persons = [Person]()
var filteredPersons = [Person]()
var isFiltering = false

override func viewDidLoad() {
super.viewDidLoad()

persons = [Person(name: "Ahmad", imageName: "img.png"), Person(name: "Harry", imageName: "img.png")]
}

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if (searchText.characters.count>0) {
isFiltering = true
filteredPersons = persons.filter {
$0.name?.range(of: searchText, options: .caseInsensitive, range: nil, locale: nil) != nil
}
print(filteredPersons)
}
else
{
isFiltering = false
filteredPersons = persons
}
self.tableviewww.reloadData()
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return isFiltering == true ? filteredPersons.count : persons.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//...

// getting the current person
let currentPerson = isFiltering == true ? filteredPersons[indexPath.row] : persons[indexPath.row]

// do the rest of the implementation...
//...
}

Note that this is Swift 3 Code.

Performing segue after filtering in uitableview

This is happening because you are using shoutout Array in prepare(for segue even when filtering.

destination.shoutoutSelected = shoutout[(tableView.indexPathForSelectedRow?.row)!]

Change it to

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "shoutoutDetailSegue" {
if let destination = segue.destination as? shoutoutDetailViewController {
// Here `Model` should be your class or struct
destination.shoutoutSelected = sender as! Model
}
}

OR move your logic here from didSelectRowAt

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "shoutoutDetailSegue" {
if let destination = segue.destination as? shoutoutDetailViewController {
if isFiltering == true {
object = filteredShoutout[indexPath.row]
} else {
object = shoutout[indexPath.row]
}
destination.shoutoutSelected = object
}
}

Filtering Array and displaying in Table View?

Your filtered also type of [Exercise] and you need to filter it like.

var filtered = [Exercise]() 

self.filtered = exercises.filter { $0.name == searchText }

Here $0 is type of Exercise object, so you need to access its property name using $0.name.

Edit: If you want filtered as type of [String] with only name then you can need to use both map and filter like this.

self.filtered = exercises.filter { $0.name == searchText }.map { $0.name }

OR

self.filtered = exercises.map { $0.name }.filter { $0 == searchText }

OR directly using filterMap as @dfri suggested.

self.filtered = exercises.flatMap{ $0.name == searchText ? $0.name : nil }

how to filter section wise row in tableview?

To search data inside the model you can use the Filter function.
Same way as you are showing data inside tableview cell. You can search using the filter function and show data inside based on the search flag.

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {

guard let searchText = searchText, !searchText.isEmpty else {
return
}

searchData = cat?.category.map({
$0.skill.filter({$0.vName == searchText})
}).filter({ $0.count > 0 })

searching = true
self.searchedSkillsResults = searchData
tableView.reloadData()
}

//Check here if your search is true and show skills array else original array results with search false.

reference link: how to search data from model in swift?

//below link help you to search within nested array.
Swift Filter Nested Array

My sample code which is working:

import Foundation
struct Welcome: Codable {
let category: [Category]
}

struct Category: Codable {
let vName: String
let skill: [Skill]

enum CodingKeys: String, CodingKey {
case vName,skill
}
}

struct Skill: Codable {
let vName: String

enum CodingKeys: String, CodingKey {

case vName
}
}

var cat : Welcome?

private func parseJSON(){

guard let path = Bundle.main.path(forResource: "category", ofType:
"json")else{
return
}

let url = URL(fileURLWithPath: path)

print(url)

do {
let jsonData = try Data(contentsOf: url)
cat = try JSONDecoder().decode(Welcome.self, from: jsonData)
if let result = cat {
print(result)
}
} catch {
print("Error is \(error)")
}
}

parseJSON()
//print(cat)

let searchText = "Section 0 Row 1"
let result = cat?.category.map({
$0.skill.filter({$0.vName.lowercased().contains(searchText.lowercased())})
}).filter({ $0.count > 0 })

//output = Optional([[Skill(vName: "Section 0 Row 1")]])

How to preserve the original indexPath.row after applying filters to Table View?

Thank you all for the comments/advice! Instead of connecting the data in the view controllers through the indexPath, I used a document ID that is consistent with the data flowing between my view controllers. This works with all of my filtering.

This is in my first ViewController:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

if let indexPath = tableView.indexPathForSelectedRow {

let ingredientsVC = segue.destination as! IngredientsViewController
let documentID = mealPlan[indexPath.row].docID
ingredientsVC.currentMealPlanIndex = indexPath.row
ingredientsVC.passedDocID = documentID!
}
}

And this is in my second ViewController:

// This variable references the unique Document ID
var passedDocID = ""

// This is how I use that document ID to get a reference to the appropriate data
let selectedMealPlanIndex = mealPlan.firstIndex(where: {$0.docID == passedDocID})
let currentMealPlan = mealPlan[selectedMealPlanIndex!]

TableView how to filter certain cells from the table

Problem - With UITableView, the method which is fired first is numberOfRowsInSection this will tell the TableView how many cell are there in the TableView, after getting the numbers the method cellForRowAtIndexPath will get fired which will used to design your TableView.

Now you are returning a count lets say 10 in numberOfRowsInSection out of which you just want to show 5 cell lets say those 5 are meal.veg == true, based on your filter meal.veg which is not possible as you need to return a cell from cellForRowAtIndexPath.

Solution - To resolve it, before reloading your table view you need to filter your array and filter out those results which are having a value meal.veg == true, then you need to pass the count of your filtered array in numberOfRowsInSection and as per that filtered array you can design your cell from cellForRowAtIndexPath

Is it better to hide a tableviewcell or filter in data source? (performance issue)

I would recommend to let the table view data source methods to deal with a filtered version of timeline. However, do not do this in cellForRowAt method because we need to do it one time but not for each cell drawing.

So, what you could do is to declare filteredTimeline and do the filter one time in the viewDidLoad method (for instance):

class TableViewController: UIViewController {
// ...
var filteredTimeline // as the same type of `timeline`

override func viewDidLoad() {
// ...

filteredTimeline = timeline?.postObjects?.filter{!($0.hidden ?? false)}

// ...
}

// Returning only the number of visible cells
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredTimeline.count ?? 0
}

// And creating cells for only visible rows
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let post = filteredTimeline?.postObjects?.filter{!($0.hidden ?? false)}[indexPath.row] {
return (tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell).with(post: post, timelineController: self, darkMode: isDarkMode())
}
}

// ...
}

In case of there is a better place to filteredTimeline = timeline?.postObjects?.filter{!($0.hidden ?? false)} rather than viewDidLoad, you might need to call tableView.reloadData().

An alternative you could do:

if you think that you don't need the original timeline you could filter it itself:

timeline = timeline?.postObjects?.filter{!($0.hidden ?? false)}
tableView.reloadData()

and you will not need an extra filtered array.


Extra tip:

In case of returning 0.0 value in heightForRowAt method for a certain row, cellForRowAt will not even get called; For example:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return indexPath.row == 0 ?? 0.0 : 100.0
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// ...
}

At this point, cellForRowAt should get called only one time because the height for the first row is 0.0.



Related Topics



Leave a reply



Submit