Filter items to section - swift

I want to filter items with property isCompleted = true to section with name Completed and non completed items to ToDo. How to render items?
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return manager.tasks.filter({$0.isCompleted == false}).count
} else {
return manager.tasks.filter({$0.isCompleted}).count
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0:
return "ToDo"
case 1:
return "Completed"
default:
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Keys.cell.rawValue, for: indexPath) as! ToDoCell
let currentItem = manager.tasks[indexPath.row]
cell.titleLabel.text = currentItem.taskName
cell.descriptionLabel.text = currentItem.description
if manager.tasks[indexPath.row].description?.isEmpty ?? false {
cell.descLabelBottomConstraint.constant = 0
}
let accessoryType: UITableViewCell.AccessoryType = currentItem.isCompleted ? .checkmark : .none
cell.accessoryType = accessoryType
return cell
}
I guess I need to filter items into two different arrays? But which way is the most correct?

Never filter things in numberOfRowsInSection. Don't do that, this method is called very often.
Create a model
struct Section {
let title : String
var items : [Task]
}
Declare the data source array
var sections = [Section]()
In viewDidLoad populate the array and reload the table view
sections = [Section(title: "ToDo", items: manager.tasks.filter{!$0.isCompleted}),
Section(title: "Completed", items: manager.tasks.filter{$0.isCompleted})]
tableView.reloadData()
Now the datasource methods become very clean (and fast)
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].items.count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section].title
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Keys.cell.rawValue, for: indexPath) as! ToDoCell
let currentItem = sections[indexPath.section].items[indexPath.row]
cell.titleLabel.text = currentItem.taskName
cell.descriptionLabel.text = currentItem.description
if currentItem.description?.isEmpty ?? false {
cell.descLabelBottomConstraint.constant = 0
} // you have to add an else clause to set the constraint to the default value
cell.accessoryType = currentItem.isCompleted ? .checkmark : .none
return cell
}
It would be still more efficient to filter the items O(n) with a partition algorithm
let p = manager.tasks.partition(by: { $0.completed })
sections = [Section(title: "ToDo", items: Array(manager.tasks[p...])),
Section(title: "Completed", items: Array(manager.tasks[..<p]))]
tableView.reloadData()

You can create 2 properties completed and notCompleted in the Manager and use them as dataSource of the tableView.
class Manager {
lazy var completed: [Task] = {
return tasks.filter({ !$0.isCompleted })
}()
lazy var notCompleted: [Task] = {
return tasks.filter({ $0.isCompleted })
}()
}
UITableViewDataSource and UITableViewDelegate methods,
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == 0 ? manager.notCompleted.count : manager.completed.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return section == 0 ? "Todo" : "Completed"
}

You want your original dataSource to be an array of the 2 different arrays (one with completed and one that is not completed.) [[]]
I found This one that seems pretty solid. However, it returns an dictionary, but i rewrote it slightly for you:
extension Sequence {
func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [[Iterator.Element]] {
return Dictionary.init(grouping: self, by: key).map({$0.value})
}
}
This way when you are in title header or cellForRowAt you can call it by manager.task[indexPath.section][indexPath.item]

Related

Issue displaying search results

Essentially I have tableview with JSON data and a search controller for the user to quickly find values from the response. While the tableview data loads initially when attempting to search I get the error index out of range. The error occurs inside the cellForRowAt function. The following is what I am currently doing:
var sections = [customerListSection]()
var structure = [customerList]()
var searchsections = [customerListSection]()
var searchstruct = [customerList]()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section = sections[section]
let searchsection = searchsections[section]
if isFiltering() {
return searchsection.count
}
return section.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let item = sections[indexPath.section].items[indexPath.row]
let customerList: customerList
if isFiltering() {
customerList = searchstruct[indexPath.row]
} else {
customerList = item
}
return cell
}
struct customerListSection {
let customerType : String
var items : [customerList]
}
struct customerList: Decodable {
let customerid: Int
let customer: String
let type: String
}
From your code, it seems that there are multiple sections, so you need to implement numberOfSections(in:)method.
If you do not implement this method, the table configures the table with one section.)
https://developer.apple.com/documentation/uikit/uitableviewdatasource/1614860-numberofsections
And since the number of rows is the number of items, not the number of sections, the tableView(_:numberOfRowsInSection:) method should return items.count.
The following is the modified code:
override func numberOfSections(in tableView: UITableView) -> Int {
if isFiltering() {
return searchsections.count
}
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
return searchsections[section].items.count
}
return sections[section].items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let customerList: customerList
if isFiltering() {
customerList = searchsections[indexPath.section].items[indexPath.row]
} else {
customerList = sections[indexPath.section].items[indexPath.row]
}
cell.textLabel?.textAlignment = .left
cell.selectionStyle = .none
cell.textLabel?.font = .boldSystemFont(ofSize: 20)
cell.textLabel?.numberOfLines = 0
cell.textLabel?.lineBreakMode = .byWordWrapping
cell.textLabel!.text = customerList.customer
return cell
}

How to populate UITableView with 2 sections

My issue is with the NumberOfRowsInSection that should read two values.
let sections = ["Apples", "Oranges"]
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//Option A)
if apples.count & oranges.count != 0 {
return apples.count & oranges.count
} else {
return 0
}
//Option B)
if sections[0] == "Apples" {
return apples.count
}
if sections[1] == "Oranges"{
return oranges.count
}
return 0
}
None of the options work, crashing because it doesn't get the amount of things of one of each other... on the CellForRowAt.
Plus somebody knows how to get the title of those sections as well?
numberOfRows will be called for each section so you need to return the correct value based on the current section being queried:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == 0 ? apples.count : oranges.count
}
You need a similar pattern in cellForRowAt and all of the other data source/delegate methods:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", indexPath: indexPath
if indexPath.section == 0 {
cell.titleLabel.text = apples[indexPath.row]
} else {
cell.titleLabel.text = oranges[indexPath.row]
}
return cell
}
This is simplified and makes lots of assumptions but it gives you the right idea.
For the headers, you need:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section]
}
This answer also assumes you just have the two sections based on two array variables. This is far from ideal. You really should have a single data structure with all of the proper structure. Then no assumptions need to be made. You can add more sections and rows and not have to change any table view code.

How do I create two different sections on table?

I am trying to separate these data into two different sections :
–one section with movies of runtime more than 120 mins, another for the rest.
The problem is I am pulling data from the same array. How can I apply conditional statement/logic and insert into each section depending on the value? I am able to pull data from the different arrays and insert into the different sections however. Please guide me. Thank you
import UIKit
class ViewController: UIViewController,
UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section:
Int) -> Int {
return movieList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : MovieCell = tableView.dequeueReusableCell(withIdentifier: "MovieCell", for: indexPath) as! MovieCell
let p = movieList[indexPath.row]
cell.nameLabel.text = p.movieName
cell.runTimeLabel.text = "\(p.runtime) mins"
cell.movieImageView.image = UIImage(named: p.imageName)
return cell
}
var movieList : [Movie] = []
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
movieList.append(Movie(movieName:"Oklahoma Crude",
movieDesc:"",
runtime:115,
imageName:"movie_oklahoma"))
movieList.append(Movie(movieName:"Bootleggers",
movieDesc:"",
runtime:140,
imageName:"movie_bootleggers"))
movieList.append(Movie(movieName:"Superdad",
movieDesc:"",
runtime:112,
imageName:"movie_superdad"))
// Do any additional setup after loading the view, typically from a nib.
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete{
movieList.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
#IBAction func editButton(_ sender: Any) {
if !self.tableView.isEditing
{
(sender as AnyObject).setTitle("Done", for: .normal)
tableView.setEditing(true, animated: true)
}
else
{
(sender as AnyObject).setTitle("Edit", for: .normal)
tableView.setEditing(false, animated: true)
} }
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let m = movieList[sourceIndexPath.row]
movieList.remove(at:sourceIndexPath.row)
movieList.insert(m, at:destinationIndexPath.row)
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
let headerTitles = ["More than 120 minutes", "Others"]
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section < headerTitles.count {
return headerTitles[section]
}
return nil
}
}
Suppose with 120 will be section = 0
func tableView(_ tableView: UITableView, numberOfRowsInSection section:
Int) -> Int {
let arr120 = movieList.filter { $0.runtime >= 120 }
let arrLess120 = movieList.filter { $0.runtime < 120 }
return section == 0 ? arr120.count : arrLess120.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let arr120 = movieList.filter { $0.runtime >= 120 }
///..
}
else {
let arrLess120 = movieList.filter { $0.runtime < 120 }
////....
}
Howver this not the perfest solution , you can organize your data source to
struct AllMovies {
let title:String // ex. above 120
let arr:[Movie]
}
It is better to have 2 arrays - one containing the movies shorter than 120mins and one containing the movies longer than 120mins. You should create those two arrays from the movieList array:
var lessThan120: [Movie]!
var moreThan120: [Movie]!
override func viewDidLoad() {
super.viewDidLoad()
movieList.append(Movie(movieName:"Oklahoma Crude",
movieDesc:"",
runtime:115,
imageName:"movie_oklahoma"))
movieList.append(Movie(movieName:"Bootleggers",
movieDesc:"",
runtime:140,
imageName:"movie_bootleggers"))
movieList.append(Movie(movieName:"Superdad",
movieDesc:"",
runtime:112,
imageName:"movie_superdad"))
let count = movieList.partition(by: {$0.runtime > 120})
lessThan120 = Array(movieList[0..<count])
moreThan120 = Array(movieList[count...])
}
Then implementing the data source methods would be very simple:
func tableView(_ tableView: UITableView, numberOfRowsInSection section:
Int) -> Int {
if section == 0 {
return moreThan120.count
} else if section == 1 {
return lessThan120.count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : MovieCell = tableView.dequeueReusableCell(withIdentifier: "MovieCell", for: indexPath) as! MovieCell
let p = indexPath.section == 0 ? moreThan120[indexPath.row] : lessThan120[indexPath.row]
cell.nameLabel.text = p.movieName
cell.runTimeLabel.text = "\(p.runtime) mins"
cell.movieImageView.image = UIImage(named: p.imageName)
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
var movieList = [String]()
var movieListGreaterThen120 = [String]()
var movieListSmallerThen120 = [String]()
for item in movieList {
if item.runtime > 120 {
movieListGreaterThen120.append(item)
}else {
movieListSmallerThen120.append(item)
}
}
//MARK: - tableView DataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section:
Int) -> Int {
if section == 0 {
return movieListGreaterThen120.count
}else {
return movieListSmallerThen120.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : MovieCell = tableView.dequeueReusableCell(withIdentifier: "MovieCell", for: indexPath) as! MovieCell
if indexPath.section == 0 {
//Load your movieListGreaterThen120 Data
}else {
//Load your movieListSmallerThen120 Data
}
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
"movieList" array can be filtered out depending on the runtime value.
if indexPath.section == 0 {
movieList.filter { $0.runtime >= 120 }
}
else {
movieList.filter { $0.runtime < 120 }
}

How can I populate multiple tableview sections when the contents of the arrays are of different types?

I'm having a bit of a polymorphism issue. I've got three arrays aList, bList and cList that contain different types of equipment "A", "B", "C".
I want to populate a tableviewcontroller with 3 sections for aList, bList, cList. But i'm struggling a bit when I get to:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
The problem i'm having is that ideally i'd have three arrays within one array eg. section = [aList, bList, cList).
In my singleton "A", "B", "C" are structs and represent a type of equipment and so I changed them into classes and subclassed them from a "Equipment" superclass and then tried to create an array of the superclass "Equipment" but that didn't work either.
Advice would be great.
class EquipmentListController: UITableViewController {
var model = SingletonModel.sharedInstance
var aList:[SingletonModel.A] = []
var bList:[SingletonModel.B] = []
var cList:[SingletonModel.C] = []
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
aList = model.AList
bList = model.BList
cList = model.CList
sections.append(aList)
sections.append(bList)
sections.append(cList)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section{
case 1:
self.tableView.rowHeight = 70
return aList.count
case 2:
return bList.count
case 3:
return cList.count
default:
return 0
}
}
My problem is below with the line let equipment: SingletonModel.A (or B or C) - basically i've got three separate arrays and each is a different type.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
guard let cell = tableView.dequeueReusableCell(withIdentifier: "equipmentcell", for: indexPath) as? EquipmentCell else
{
}
let equipment: SingletonModel.A (or B or C) = changeDataSource(indexPath: indexPath as NSIndexPath)
cell.equipmentTitle.text = equipment.equipmentName
cell.accessoryType = .disclosureIndicator
return cell
}
func changeDataSource(indexPath: NSIndexPath) -> SingletonModel.A, B or C
{
var equipment:SingletonModel.A (B or C)
equipment = A, B or C [indexPath.row]
return equipment
}
func tableView(tableView: UITableView!, titleForHeaderInSection section: Int) -> String!
{
switch section
{
case 1:
return "A"
case 2:
return "B"
case 3:
return "C"
default:
return nil
}
}
override func tableView(_ tableView: UITableView,
heightForHeaderInSection section: Int) -> CGFloat
{
return 40
}
}
Thanks
First you need to fix section numbering: sections are numbered from zero, not from one:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return aList.count
case 1:
return bList.count
case 2:
return cList.count
default:
return -1 // Fail fast
}
}
Next, change the way you set row height:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// Rows in section 0 are taller
return indexPath.section == 0 ? 70.0 : 50.0;
}
Now you can set your cell up using A, B, or C:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "equipmentcell", for: indexPath) as? EquipmentCell else
{
return nil
}
var equipment : BaseOfABC = nil
switch indexPath.section {
case 0:
equipment = SingletonModel.A
case 1:
equipment = SingletonModel.B
case 2:
equipment = SingletonModel.C
default:
assert(indexPath.section < 3, "Unknown section")
}
cell.equipmentTitle.text = equipment.equipmentName
cell.accessoryType = .disclosureIndicator
return cell
}
In indexPath you have the row and the section of the cell :
indexPath.row
indexPath.section
You can switch on indexPath.section
For each (equipment) type you can create your own cell type.
In
tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
Switch on indexPath.section, create the matching cell type and set its attributes from the matching (equipment) array.

Swift IndexPath counting continuous over multiple sections?

I have multiple sections in a UITableView. I am using the IndexPath of the cells to set some values. What I am asking is if at the start of a new section, the counting of the IndexPaths restarts to zero. Any help is greatly appreciated. Thanks!
it does. If you implement the basic example below, you'll see how the row index restarts from 0 in each section
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var myTableView: UITableView!
let myDataSource = ["Andrew", "Anthony", "Bill", "Brad", "Charlie", "Craig"]
override func viewDidLoad() {
super.viewDidLoad()
myTableView.delegate = self
myTableView.dataSource = self
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell
cell.myCellLabel.text = myDataSource[indexPath.row + indexPath.section*2] + " [Row = \(indexPath.row), Section = \(indexPath.section)]"
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
var title = ""
switch section {
case 0:
title = "A"
case 1:
title = "B"
case 2:
title = "C"
default:
break
}
return title
}
}