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
}
Related
everyone! I have a problem when I am connecting JSON with the table view. There are two mistakes in the functions:
Value of type 'BalanceStruct?' has no member 'count'
and
Value of type 'BalanceStruct?' has no member 'indexPath'
import UIKit
class BalanceViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var balances: BalanceStruct?
override func viewDidLoad() {
super.viewDidLoad()
ServerManager.shared.getBalanceList(token: UserDefaults.standard.value(forKey: "token") as! String, { (balanceList) in
self.balances = balanceList
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.reloadData()
})
{(error) in print(error)}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return balances.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let balance = balances.indexPath.row
let cell = tableView.dequeueReusableCell(withIdentifier: "BalanceCell", for: indexPath) as! BalancesViewCell
cell.configure(balances: balance)
return cell
}
}
and the problem is in this part:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return balances.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let balance = balances.indexPath.row
let cell = tableView.dequeueReusableCell(withIdentifier: "BalanceCell", for: indexPath) as! BalancesViewCell
cell.configure(balances: balance)
return cell
}
BalanceStruct is a model
struct BalanceStruct: Codable {
let content: [ContentBS]
let pageable: Pageable
let totalPages, totalElements: Int
let last: Bool
let sort: Sort
let numberOfElements: Int
let first: Bool
let size, number: Int
let empty: Bool
}
// MARK: - Content
struct ContentBS: Codable {
let id: Int
let dateCreated: String
let dateUpdated: String?
let name: String
let balance: Int
}
How can I fix it? Thank you!
The table view data source must be an array (or at least a collection type)
var balances = [BalanceStruct]()
Then the data source methods are
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return balances.count // works
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let balance = balances[indexPath.row] // must be index subscription
let cell = tableView.dequeueReusableCell(withIdentifier: "BalanceCell", for: indexPath) as! BalancesViewCell
cell.configure(balances: balance)
return cell
}
However if the content array is supposed to be displayed replace
var balances = [BalanceStruct]()
with
var contentBS = [ContentBS]()
and
self.balances = balanceList
with
self.contentBS = balanceList.content
Now the data source methods are
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contentBS.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let balance = contentBS[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "BalanceCell", for: indexPath) as! BalancesViewCell
cell.configure(balances: balance)
return cell
}
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]
I am currently building an Application which shows all the faculties that are available in my college. I have the data in a .plist file which I have read and populated in the UITableView. I am currently trying to show some details such as prefix and Name in the cell which when clicked on Expands to show more details about the faculty.
My TableViewController Code is shown below:
struct cellData {
var opened = Bool()
var Name = String()
var Prefix = String()
var sectionData = [String]()
}
class TableViewController: UITableViewController {
//MARK: - Initialize variables
var dataManager = DataManager()
private var header = [String]()
private var facultyDetails = [[String: String]]()
var tableViewData = [cellData]()
#IBOutlet var facultyTableView: UITableView!
//MARK: - view did load
override func viewDidLoad() {
super.viewDidLoad()
for data in dataManager.allSheet1s() {
let dataInCell = cellData(opened: false, Name: data.item2!,
Prefix: data.item1!, sectionData [data.item0!,
data.item2!,
data.item1!,
data.item3!,
data.item4!,
data.item5!,
data.item6!,
data.item7!,
data.item8!,
data.item9!,
data.item10!
])
tableViewData.append(dataInCell)
facultyTableView.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: "CustomCell")
}
}
// MARK: - Table view delegate
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return tableViewData.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell") as! CustomCell
if indexPath.row == 0 {
cell.EmpID.text = tableViewData[indexPath.section].sectionData[0]
cell.Name.text = tableViewData[indexPath.section].sectionData[1]
cell.Prefix.text = tableViewData[indexPath.section].sectionData[2]
cell.SchoolName.text = tableViewData[indexPath.section].sectionData[3]
cell.BuildingName.text = tableViewData[indexPath.section].sectionData[4]
cell.FloorNo.text = tableViewData[indexPath.section].sectionData[5]
cell.CabinLocation.text = tableViewData[indexPath.section].sectionData[6]
cell.RoomNo.text = tableViewData[indexPath.section].sectionData[7]
cell.CabinNo.text = tableViewData[indexPath.section].sectionData[8]
cell.IntercomNumber.text = tableViewData[indexPath.section].sectionData[9]
cell.Email.text = tableViewData[indexPath.section].sectionData[10]
return cell
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableViewData[indexPath.section].opened == true {
tableViewData[indexPath.section].opened = false
let sections = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(sections, with: .right)
}
else {
tableViewData[indexPath.section].opened = true
let sections = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(sections, with: .left)
}
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if tableViewData[indexPath.section].opened == true {
return 400
}
else {
return 70
}
}
}
Simulator
Physical Device
Right now you can see in the simulator that it works perfectly fine. But when I load it up in the physical device the height of each cell in the rows are clipping through the other cells below it.
Modify your cell for row code by adding cell.clipsToBounds = true
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell") as! CustomCell
cell.clipsToBounds = true
if indexPath.row == 0 {
cell.EmpID.text = tableViewData[indexPath.section].sectionData[0]
cell.Name.text = tableViewData[indexPath.section].sectionData[1]
cell.Prefix.text = tableViewData[indexPath.section].sectionData[2]
cell.SchoolName.text = tableViewData[indexPath.section].sectionData[3]
cell.BuildingName.text = tableViewData[indexPath.section].sectionData[4]
cell.FloorNo.text = tableViewData[indexPath.section].sectionData[5]
cell.CabinLocation.text = tableViewData[indexPath.section].sectionData[6]
cell.RoomNo.text = tableViewData[indexPath.section].sectionData[7]
cell.CabinNo.text = tableViewData[indexPath.section].sectionData[8]
cell.IntercomNumber.text = tableViewData[indexPath.section].sectionData[9]
cell.Email.text = tableViewData[indexPath.section].sectionData[10]
return cell
}
return cell
}
And override a method
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 70
}
I'm trying to section my Tableview data based on a Key in my Firebase database.
I'm able to section everything properly based on the key (itemPreset).
I'm having trouble assigning the reusable cells to their sections.
The cells keep repeating themselves with the same text value in each cell.
The amount of rows per cell is correct and the section header title is correct.
Here is my code -
var subCategories = [SubCategoryCellInfo]()
var sectionsArray = [String]()
func querySections() -> [String] {
for selection in subCategories {
let subCategory = selection.itemPreset
sectionsArray.append(subCategory ?? "")
}
let uniqueSectionsArray = Set(sectionsArray).sorted()
return uniqueSectionsArray
}
func queryItemPreset(section:Int) -> [Int] {
var sectionItems = [Int]()
for selection in subCategories {
let itemPreset = selection.itemPreset
if itemPreset == querySections()[section] {
sectionItems.append(querySections().count)
}
}
return sectionItems
}
func numberOfSections(in tableView: UITableView) -> Int {
return querySections().count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return querySections()[section]
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering(){
return filtered.count
}
return queryItemPreset(section: section).count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let subCell = tableView.dequeueReusableCell(withIdentifier: "subCell", for: indexPath) as! SubCategoryTableViewCell
let section = queryItemPreset(section: indexPath.section)
let task = section[indexPath.row]
let sub: SubCategoryCellInfo
if isFiltering(){
sub = filtered[task]
}
else{
sub = subCategories[task]
}
subCell.nameOfLocationText.text = sub.itemPreset
return subCell
}
SubCategoryCellInfo:
class SubCategoryCellInfo{
var itemPreset: String?
init(itemPreset:String?){
self.itemPreset = itemPreset
}
}
Solution:
I grouped the array into sections based on itemPreset and then used that section
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let subCell = tableView.dequeueReusableCell(withIdentifier: "subCell", for: indexPath) as! SubCategoryTableViewCell
let groupedDictionary = Dictionary(grouping: subCategories) { (person) -> String in
return person.itemPreset ?? ""
}
var grouped = [[SubCategoryCellInfo]]()
let keys = groupedDictionary.keys.sorted()
keys.forEach { (key) in
grouped.append(groupedDictionary[key]!)
}
let task = grouped[indexPath.section]
let sub: SubCategoryCellInfo
if isFiltering(){
sub = filtered[indexPath.row]
}
else{
sub = task[indexPath.row]
}
subCell.nameOfLocationText.text = sub.itemPreset
return subCell
}
Inside your SubCategoryTableViewCell write this code.
override func prepareForReuse() {
super.prepareForReuse()
nameOfLocationText.text = nil
}
Solution: Group the array into sections based on itemPreset and then use that section.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let subCell = tableView.dequeueReusableCell(withIdentifier: "subCell", for: indexPath) as! SubCategoryTableViewCell
let groupedDictionary = Dictionary(grouping: subCategories) { (person) -> String in
return person.itemPreset ?? ""
}
var grouped = [[SubCategoryCellInfo]]()
let keys = groupedDictionary.keys.sorted()
keys.forEach { (key) in
grouped.append(groupedDictionary[key]!)
}
let task = grouped[indexPath.section]
let sub: SubCategoryCellInfo
if isFiltering(){
sub = filtered[indexPath.row]
}
else{
sub = task[indexPath.row]
}
subCell.nameOfLocationText.text = sub.itemPreset
return subCell
}
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 }
}