Home > Software engineering >  UITableViewCell Delete Row Causing Index Out of Range
UITableViewCell Delete Row Causing Index Out of Range

Time:09-07

Here is my code :

    class SchedulerViewController: UIViewController, UITableViewDelegate, UITableViewDataSource,
    scheduleCellDelegate
    {
    
        
        var scheduleArray : Array<Array<String>>?
        var scheduler : [String : Array<Array<String>>]?
        var deviceID : String = ""
        let retrievedString = KeychainWrapper.standard.string(forKey: "token")
    
        var day = ""
        var dayNum = 0
      
        @IBOutlet weak var spinner: UIActivityIndicatorView!
        @IBOutlet var buttons: [UIButton]!
        @IBOutlet weak var scheduleView: UITableView!
        
        var header : HTTPHeaders? = nil
        var ScheduleURL : Dictionary<String, String>?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            scheduleView.delegate = self
            scheduleView.dataSource = self
            spinner.isHidden = true
            //scheduleView.allowsSelection = false
            scheduleView.register(UINib(nibName: "schedulerCell", bundle: nil), forCellReuseIdentifier: "schedulerCell")
            self.getData()
            
        }
        
        func getData(){

                AFFunctions.getAFRequest(ofType: ScheduleResponse.self, url: ScheduleURL!["GET"]!) { responseData, statusCode in
                    print(responseData?.data?.scheduler, statusCode)
                    self.scheduler = responseData?.data?.scheduler
                    DispatchQueue.main.async {
        
                        self.scheduleView.reloadData()
                    }
                    
                }
            }
            
           
            var buttonNum : Int?
            
            @IBAction func daySelected(_ sender: UIButton) {
                
                self.buttons.forEach { $0.tintColor = ($0 == sender) ? UIColor.orange : UIColor.systemTeal }
                
                self.dayNum = sender.tag
                switch dayNum {
                case 0 : self.day = "Sunday"
                case 1 : self.day = "Monday"
                case 2 : self.day = "Tuesday"
                case 3 : self.day = "Wednesday"
                case 4 : self.day = "Thursday"
                case 5 : self.day = "Friday"
                case 6 : self.day = "Saturday"
                default : self.day = "Sunday"
           
            }
            showDetail(day : day,dayNum : dayNum)
        }
        
        func showDetail(day : String, dayNum : Int) {

            if let dayArray = scheduler?[day]
            {  
                scheduleArray = dayArray
                self.scheduleView.reloadData()
            } 
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return scheduleArray?.count ?? 0
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            
            let cell = Bundle.main.loadNibNamed("scheduleCell", owner: self, options: nil)?.first as! scheduleCell
            cell.cellDelegate = self
            cell.editBtn.tag = indexPath.row
            cell.deleteSchedule.tag = indexPath.row
            scheduleArray = scheduler![self.day]
/////////////THE BELOW STATEMENT THROWS THE ERROR ///////////////////
                if let firstLabel = self.scheduleArray?[indexPath.row][0], let secondLabel = self.scheduleArray?[indexPath.row][1] {
                DispatchQueue.main.async {
                        cell.timeLabel1.text = firstLabel
                        cell.timeLabel2.text = secondLabel
                    }
                }
            return cell
        }
        
        func didPressButton(_ tag: Int, btnType: String) {
            let deleteURL = K.delURL
            if(btnType == "delete") {
                AFFunctions.deleteAFRequest(ofType: scheduleResponse.self, url: "\(deleteURL)?day=\(self.day)&place=\(tag)") { [self]
                    responseData, statusCode in
                    if(statusCode == 200){
                        let deleteAlert = UIAlertController(title: "Deleted", message: "Device Schedule Successfully Deleted", preferredStyle: UIAlertController.Style.alert)
    
                        deleteAlert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (action: UIAlertAction!) in
                            
                            self.scheduler![self.day]?.remove(at: tag)
                            self.scheduleView.reloadData()
        
                         }))
                        self.present(deleteAlert, animated: true, completion: nil)
    
                    }
                }
            }
        }
     
    }

I am doing the changes in the array locally as data is fetched only once in ViewDidLoad() with getData function. It shows a schedule for each day of the week (7 buttons, one for each day, are linked to an Outlet Collection), with 2 buttons embedded in the custom cell nib - an edit button and a delete button. I have implemented the logic for deleting, button tags are equal to IndexPath.row which works perfectly and I am able to delete the values I want but when I can't seem to get the table reload working. Even after deleting the row data, the table doesn't update itself. I am calling reloadData after successful deletion. What am I doing wrong?

CodePudding user response:

there are 2 issues

  • when you update something on UI after a request call, you have to push the UI update process back to the main thread (tableview.reloadData() should get triggered on the main thread)
  • self.scheduleArray?[indexPath.row][0] and self.scheduleArray?[indexPath.row][1] is the issue index out of range. Because you assume it always contains 2 items without safe check.

I've refactored the code a bit as below

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "scheduleCell") else {
        return Bundle.main.loadNibNamed("scheduleCell", owner: self, options: nil)?.first as! scheduleCell
    }
    cell.cellDelegate = self
    cell.editBtn.tag = indexPath.row
    cell.deleteSchedule.tag = indexPath.row
    scheduleArray = scheduler?[self.day]
   
    guard let item = scheduleArray?[indexPath.row], item.count == 2 else {
        return cell
    }
    if let firstLabel = item.first, let secondLabel = item.last {
        cell.timeLabel1.text = firstLabel
        cell.timeLabel2.text = secondLabel
    }
    return cell
}

func didPressButton(_ tag: Int, btnType: String) {
    let deleteURL = K.delURL
    if(btnType == "delete") {
        AFFunctions.deleteAFRequest(ofType: scheduleResponse.self, url: "\(deleteURL)?day=\(self.day)&place=\(tag)") { [self]
            responseData, statusCode in
            if(statusCode == 200){
                // This has to be executed on the main thread to get tableView updated
                DispatchQueue.main.async {
                    let deleteAlert = UIAlertController(title: "Deleted", message: "Device Schedule Successfully Deleted", preferredStyle: UIAlertController.Style.alert)
                    
                    deleteAlert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (action: UIAlertAction!) in
                        self.scheduler![self.day]?.remove(at: tag)
                        self.scheduleView.reloadData()
                        
                    }))
                    self.present(deleteAlert, animated: true, completion: nil)
                }
            }
        }
    }
}

CodePudding user response:

The issue here is you are holding multiple sources of truth and fail at synchronysing them. You have:

var scheduleArray : Array<Array<String>>?
var scheduler : [String : Array<Array<String>>]?

Where both seem to hold the same information in different form. I can´t see why you are doing this from the example code you posted.

You get an error at:

self.scheduleArray?[indexPath.row][0]

because when you delete your item you are removing it from scheduler and reload your tableview. The tableview on the other hand get´s the information how many rows it should render from scheduleArray:

return scheduleArray?.count ?? 0

and these differ at that time because you didn´t assign scheduler to scheduleArray.

So 2 possible solutions here:

  • assign scheduleArray before you reload your tableview

    self.scheduler![self.day]?.remove(at: tag)
    scheduleArray = scheduler![self.day]
    

and remove the assignment in the cellForItemAt function

  • stop using scheduleArray and scheduler. Use only a single collection to hold the information.

CodePudding user response:

Solved it! Updated Code :

class SchedulerViewController: UIViewController, UITableViewDelegate, UITableViewDataSource,
scheduleCellDelegate
{

    
    var scheduleArray : Array<Array<String>>?
    var scheduler : [String : Array<Array<String>>]?
    var deviceID : String = ""
    let retrievedString = KeychainWrapper.standard.string(forKey: "token")

    var day = ""
    var dayNum = 0
  
    @IBOutlet weak var spinner: UIActivityIndicatorView!
    @IBOutlet var buttons: [UIButton]!
    @IBOutlet weak var scheduleView: UITableView!
    
    var header : HTTPHeaders? = nil
    var ScheduleURL : Dictionary<String, String>?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scheduleView.delegate = self
        scheduleView.dataSource = self
        spinner.isHidden = true
        //scheduleView.allowsSelection = false
        scheduleView.register(UINib(nibName: "schedulerCell", bundle: nil), forCellReuseIdentifier: "schedulerCell")
        self.getData()
        
        
    }
    
    func getData(){
       
      
        self.header =
        [
            "Content-Type" : "application/json",
            "Authorization": retrievedString!
        ]
        //scheduleView.register(scheduleCell.self, forCellReuseIdentifier: "scheduleCell")
       
        print(ScheduleURL!["GET"])
        AFFunctions.getAFRequest(ofType: ScheduleResponse.self, url: ScheduleURL!["GET"]!) { responseData, statusCode in
            print(responseData?.data?.scheduler, statusCode)
            self.scheduler = responseData?.data?.scheduler
            self.scheduleView.reloadData()

        }
    }
    
   
    var buttonNum : Int?
    
    @IBAction func daySelected(_ sender: UIButton) {
        
        self.buttons.forEach { $0.tintColor = ($0 == sender) ? UIColor.orange : UIColor.systemTeal }
        
        self.dayNum = sender.tag
        print(sender.tag)
        switch dayNum {
        case 0 : self.day = "Sunday"
        case 1 : self.day = "Monday"
        case 2 : self.day = "Tuesday"
        case 3 : self.day = "Wednesday"
        case 4 : self.day = "Thursday"
        case 5 : self.day = "Friday"
        case 6 : self.day = "Saturday"
        default : self.day = "Sunday"
        }
        print(day, dayNum)
        showDetail(day : day)
    }
    
    
    
    
    func showDetail(day : String) {
        
        print(day)
        print(scheduler?[day])
        if let dayArray = scheduler?[day]
        {   print(dayArray)
            scheduleArray = dayArray
            self.scheduleView.reloadData()
        }
        
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        print("111 CHECK")
        return scheduleArray?.count ?? 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = Bundle.main.loadNibNamed("scheduleCell", owner: self, options: nil)?.first as! scheduleCell
        cell.cellDelegate = self
        cell.editBtn.tag = indexPath.row
        cell.deleteSchedule.tag = indexPath.row
        if let item = scheduleArray?[indexPath.row],
           item.count > 1 {
            DispatchQueue.main.async {
                cell.timeLabel1.text = item[0]
                cell.timeLabel2.text = item[1]
            }
               }

        return cell
    }
    

  
    
    func didPressButton(_ tag: Int, btnType: String) {
        let deleteURL = K.delURL
        print("134", self.day)
        print("135", self.scheduleArray?[tag])
        print("136", scheduler?[self.day])
        print("TAG : ", tag)
        print("BTN TYPE: ", btnType)
        if(btnType == "delete") {
            AFFunctions.deleteAFRequest(ofType: scheduleResponse.self, url: "\(deleteURL)?day=\(self.day)&place=\(tag)") { [self]
                responseData, statusCode in
                print("\(deleteURL)?day=\(self.day)&place=\(tag)")
                print(responseData, statusCode)
                if(statusCode == 200){
                    self.scheduler![self.day]!.remove(at: tag)
                    DispatchQueue.main.async {
                    let deleteAlert = UIAlertController(title: "Deleted", message: "Device Schedule Successfully Deleted", preferredStyle: UIAlertController.Style.alert)

                    deleteAlert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (action: UIAlertAction!) in
                        self.showDetail(day: self.day)
//                        self.scheduleView.reloadData()
                        
                     }))
                    self.present(deleteAlert, animated: true, completion: nil)
                    
                    }
                    
                }
            }
        }
    }
 
}
  • Related