index is out of range after filtering an array - swift

I am trying to filter a song array so it only has songs where the song artistName is the same as ArtistName(Which is always the selected artist). When I run my app it throws an Fatal error: Index out of range even though in my debug console artistSongs has 4 elements. I don't understand how id be getting this crash.
override func viewDidLoad() {
super.viewDidLoad()
retriveData()
//Register nib
topSongTableView.register(TopSongTableViewCell.nib(), forCellReuseIdentifier: TopSongTableViewCell.topSongCell)
ArtistPicture.image = artistCover
ArtistLabel.text = ArtistName
//Assign TableView to self
topSongTableView.delegate = self
topSongTableView.dataSource = self
}
//Define number of rows in topSongsTableView
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
// songs of artist
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TopSongTableViewCell", for: indexPath) as! TopSongTableViewCell
let artistSongs = songs.filter{ $0.artistName == ArtistName }
print(artistSongs)
cell.TopSongLabel.text = artistSongs[indexPath.row].cleanName //CASH
cell.SongImage.image = UIImage(named: artistSongs[indexPath.row].cover)
return cell
}

Your problem is that your tableView topSongTableView expects to display 5 rows (you hard coded it) but when you're dequeuing your cells, you also filter artistSongs at the same time (it shouldn't be done here) and this produces in your case an array with less than 5 elements (4 in your case).
So here in func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell, you're trying to dequeue more cells than elements in artistSongs.
Let's say let artistSongs = songs.filter{ $0.artistName == ArtistName } produces an array of 3 elements only but your table view expects 5, when you dequeue your cells and try to access the song with artistSongs[indexPath.row], it's gonna work for the first 3 rows and then it will crash (your current crash).
Your solution is to filter the artist songs somewhere else, say in viewDidLoad, and then use this array to populate your table view safely:
// ADD THIS
var filteredSongs: [Song]()
override func viewDidLoad() {
super.viewDidLoad()
[...]
// ADD THIS
self.filteredSongs = songs.filter{ $0.artistName == ArtistName }
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// CHANGE THIS
return self.filteredSongs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TopSongTableViewCell", for: indexPath) as! TopSongTableViewCell
// REMOVE THIS
//let artistSongs = songs.filter{ $0.artistName == ArtistName }
// REMOVE THIS
//print(artistSongs)
cell.TopSongLabel.text = self.filteredSongs[indexPath.row].cleanName
cell.SongImage.image = UIImage(named: self.filteredSongs[indexPath.row].cover)
return cell
}

Related

Weird display behaviour when selecting tableview row

I have this table view, that populates through Core Data. Everything looks fine, even when scrolling, until I press and hold any row - then that row distorts like in the second image below.
(source: staticflickr.com)
(source: staticflickr.com)
Here's the code to display the tableview
override func viewDidLoad() {
super.viewDidLoad()
animateBackground(colors)
tableView.delegate = self
tableView.dataSource = self
tableView.fetchData(fetchedResultsController as! NSFetchedResultsController<NSFetchRequestResult>)
self.tableView.tableFooterView = UIView(frame: .zero)
}
func numberOfSections(in tableView: UITableView) -> Int {
guard fetchedResultsController.sections?.count != nil else { return 0 }
return fetchedResultsController.sections!.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard fetchedResultsController.sections != nil else { return 0 }
let data = fetchedResultsController.sections![section]
return data.numberOfObjects
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SkillCell", for: indexPath) as! SkillCell
configureCell(cell: cell, indexPath: indexPath as NSIndexPath)
return cell
}
func configureCell(cell: SkillCell, indexPath: NSIndexPath) {
let skill = fetchedResultsController.object(at: indexPath as IndexPath)
cell.skill = skill
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { }
}
From the code above, everything looks normal, yet this distortion occurs. Please help me to see why and how to stop it happening.
As the comment suggests, it's just a visual effect called selectionStyle of your UITableViewCell. Inside your cellForRowAt indexPath method, you can disable it like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell") as! MyCell
cell.selectionStyle = .none // this removes the effect.
return cell
}
yes you should set tableView selection style to none.
1.Select the tableview in storyboard in the right panel set selection style to none.
or
2.in your tableview cellforrow method set
tableView.selectionstyle = none

Remove index from sidemenu user type wise

I have implemented side menu in tableview and now my scenario is like, I have to manage sidemenu options as user types
Let me show my code
var items = ["Social Media Post", "Messages", "Manage User","My Account","Information","Logout"]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! MenuTableViewCell
cell.lblTitle.text = items[indexPath.row]
cell.imgMenuLogo.image = image[indexPath.row]
print(User_type)
if User_type == 2{
items.remove(at: 0)
}
return cell
}
but now i want . to de like this
if user_type == "3"{
// Social Media , Messages And Manage User options i want to remove
}
I am not able to understand how to remove from index.
Try something like this:
override func viewDidLoad() {
super.viewDidLoad()
getList()
}
func getList(){
switch userType{
case 0:
items = ["UserTypeOne_Home","UserType_One Settings","etc"]
break
case 1:
items = ["UserTypeTwo_Home","UserType_Two Settings","etc"]
break
default:
break
}
self.tableView.reloadData()
}
extension ViewController: UITableViewDelegate,UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "Some ID")
cell?.textLabel?.text = items[indexPath.row]
return cell!
}
}
Try not to change the array from within the cellForRowAt indexPath method using indexPath.row, that will not give you the result you want. Modulate the array from outside the protocol method overrides, and just call the reloadData() method.
Try to use enum of UserType and check type of current user than Make an array with default options available to every user and then append specific data in array according to user type. Hope it clarifies :)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! MenuTableViewCell
cell.lblTitle.text = items[indexPath.row]
cell.imgMenuLogo.image = image[indexPath.row]
print(User_type)
if User_type == 2{
items.remove(at: 0)
}
return cell
}
This will work, But you have done a small mistake here.
You have removed from array after setting label. So you need to remove the item from array first then do set label.
Btw, I will not recommend this method as you need to add/remove from array for every cellForRowAt method.

Swift calling a class from a view controller

I am a beginner in swift coming from java. I have a task class and a taskcollection class where it is a collection of tasks. I am trying to put the collection into a table view but I dont know how.
This is task class:
class Task{
private var description : String
init(description: String){
self.description = description
}
func getDescription() -> String{
return self.description
}
func setDescription(description: String){
self.description = description
}}
This is taskCollection class:
class TaskCollection{
private var tasks: Array<Task>
init(){
self.tasks = [Task]()
}
func getTasks() -> Array<Task>{
return self.tasks
}
func addTask(task: Task){
self.tasks.append(task)
}}
To get the taskcollection into the tableview what do i do? In java I know I have to make something like new TaskCollection(), but i dont know how to get it in the tableviews:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return ?
}
// create a cell for each table view row
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// create a new cell if needed or reuse an old one
let cell:UITableViewCell = self.tableviewTasks.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell!
// set the text from the data model
cell.textLabel?.text = self.___?_____[indexPath.row]
return cell
}
// method to run when table view cell is tapped
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("You tapped cell number \(indexPath.row).")
}
You would create an instance of the TaskCollection class somewhere in your ViewController file scope. Then you populate it somewhere else (I did this is viewDidLoad as an example). Number of rows will simply be the number of tasks in the taskCollection instance. You can access each task by using indexPath.row to get the index of the array of tasks.
class TableViewController: UITableViewController {
let taskCollection = TaskCollection()
override func viewDidLoad() {
super.viewDidLoad()
let task = Task(description: "Some Description")
taskCollection.addTask(task: task)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return taskCollection.getTasks().count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
let task = taskCollection.getTasks()[indexPath.row]
cell.textLabel?.text = task.getDescription()
return cell
}
}
First there is no need to create a TaskCollection class as it's functionality is already build in arrays so, you need to create an array of tasks
var tasksArr = [Task]()
//
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tasksArr.count
}
// create a cell for each table view row
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// create a new cell if needed or reuse an old one
let cell:UITableViewCell = self.tableviewTasks.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell!
// set the text from the data model
cell.textLabel?.text = self.tasksArr[indexPath.row].description
return cell
}
OR according to current implementation
var colc = TaskCollection()
//
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.colc.tasks.count
}
// create a cell for each table view row
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// create a new cell if needed or reuse an old one
let cell:UITableViewCell = self.tableviewTasks.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell!
// set the text from the data model
cell.textLabel?.text = self.colc.tasks[indexPath.row].description
return cell
}

How to remove item from Array?

I have a multi selected tableview.
what I am doing : when user select items, this items append to the array.
When user deselect the item from cell, this deselected items remove from array.
what I did :
My array : var selectedTagList:[Tag] = []
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tagTableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
self.selectedTagList.append(tagList![indexPath.row])
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
self.tagTableView.cellForRow(at: indexPath)?.accessoryType = .none
self.selectedTagList.remove(at: indexPath.row)
}
Any advice or sample code please ?
//DataSource and Delegate
extension PickVideoViewController : UITableViewDataSource,UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let tlist = self.tagList , !tlist.isEmpty else { return 1}
return tlist.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let tlist = self.tagList , !tlist.isEmpty else {
let cell = UITableViewCell()
cell.selectionStyle = .none
cell.backgroundColor = .clear
cell.textLabel?.textAlignment = .center
cell.textLabel?.textColor = UIColor.black
cell.textLabel?.text = "nodataavaiable".localized()
return cell }
let cell = tableView.dequeueReusableCell(withIdentifier: "TagCell", for: indexPath) as! TagCell
cell.tagName.text = tlist[indexPath.row].tag
cell.accessoryType = cell.isSelected ? .checkmark : .none
cell.selectionStyle = .none // to prevent cells from being "highlighted"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tagTableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
self.selectedTagList.append(tagList![indexPath.row])
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
self.tagTableView.cellForRow(at: indexPath)?.accessoryType = .none
self.selectedTagList.remove(at: <#T##Int#>)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let tlist = self.tagList , !tlist.isEmpty else {return tableView.frame.height }
return 40
}
}
self.selectedTagList.remove(at: indexPath.row)
indexPath.row is the wrong value to remove. Since your data source populates based on tagList and not selectedTagList, you need to get the item out of tagList and find the equivalent item in selectedTagList.
You don't show what type of object is in tagList, but you will probably need to make them conform to Equatable so you can do this lookup. Once you do, you should have something like this:
let deselectedTag = self.tagList[indexPath.row]
// You will need the items in `tagList` to conform to `Equatable` to do this
guard let indexOfDeselectedTag = self.selectedTagList.index(of: deselectedTag else) {
// Data inconsistency: the item wasn't found in selectedTagList
return
}
self.selectedTagList.remove(at: indexOfDeselectedTag)
You don't need to maintain a list of selected items. You already have all of the items and the tableView can tell you which rows/items are selected.
https://developer.apple.com/documentation/uikit/uitableview/1614864-indexpathsforselectedrows
Discussion
The value of this property is an array of index-path
objects each identifying a row through its section and row index. The
value of this property is nil if there are no selected rows.
You are trying to reinvent the wheel. Always check the documentation for existing functionality.
If you then want a list of selected items from this you just create an array of the items at those index paths, something like this: (untested)
let selectedItems = tableView.indexPathsForSelectedRows().map {
self.tagList[$0.row]
}
This code will iterate over the index paths and return the item from the array at each one. (this is untested, you may need to use flatMap as you are changing the type)

How do you add a second section with a different type of prototype cell to a tableview in Swift?

I have a tableview with 2 prototype cells in a view controller. I want each cell to display data from different arrays.
Below is my code. I can get the tableview to show jobs or schools, but not both. I don't understand how to make the table display cell1 (jobs) and then cell2 (schools) when each cell contains different data sources.
screenshot
Import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let jobs = ["McDonalds", "Hardees", "Taco Bell"]
let schools = ["Univ of CO", "Univ of TX", "Univ of CA"]
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return jobs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "JobsCell", for: indexPath) as! Jobs
let job = jobs[indexPath.row]
cell.jobLbl.text = job
return cell
}
}
Answer: I fixed this by adding numberOfSections function (thanks Robert). Updated code below if anyone else has this question:
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let jobs = ["McDonalds", "Hardees", "Taco Bell"]
let schools = ["Univ of CO", "Univ of TX", "Univ of CA", "Univ of Camdenton"]
override func viewDidLoad() {
super.viewDidLoad()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (section == 0) {
return jobs.count
} else {
return schools.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "JobsCell", for: indexPath) as! Jobs
let job = jobs[indexPath.row]
cell.jobLbl.text = job
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "SchoolsCell", for: indexPath) as! Schools
let school = schools[indexPath.row]
cell.schoolLbl.text = school
return cell
}
}
}
It sounds like you want to display one section with cells for jobs followed by a second section with cells for schools. If that's the case, you'll need to set the number of sections to 2, then rewrite your delegate functions to respond appropriately depending on the section number.
So numberOfRowsInSection is going to have to check the section number, then return the number of rows for that specific section. And cellForRowAt will need to check the section, then set up and return either a job or a school cell for the given row.
Here's what that would look like in your example:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let jobs = ["McDonalds", "Hardees", "Taco Bell"]
let schools = ["Univ of CO", "Univ of TX", "Univ of CA"]
override func viewDidLoad() {
super.viewDidLoad()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch(section) {
case 0: return jobs.count
default: return schools.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch(indexPath.section) {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "JobsCell", for: indexPath) as! Jobs
let job = jobs[indexPath.row]
cell.jobLbl.text = job
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: "SchoolsCell", for: indexPath) as! Schools
let school = schools[indexPath.row]
cell.schoolLbl.text = school
return cell
}
}
}
And here's the output in the simulator: