0

I have Alarm Clock App. User can add alarms. If I add too much alarms and scroll down I will see that

enter image description here

If i tried to use UISwitch (only buttom cell) I will crash the app and get Error:

Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 8 beyond bounds [0 .. 7]'

I think that the problem inside of array. How to resolve it?

import UIKit

class MainAlarmViewController: UITableViewController{

    var alarmDelegate: AlarmApplicationDelegate = AppDelegate()
    var alarmScheduler: AlarmSchedulerDelegate = Scheduler()
    var alarmModel: Alarms = Alarms()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.allowsSelectionDuringEditing = true
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        alarmModel = Alarms()
        tableView.reloadData()
        //dynamically append the edit button
        if alarmModel.count != 0 {
            self.navigationItem.leftBarButtonItem = editButtonItem
        }
        else {
            self.navigationItem.leftBarButtonItem = nil
        }
        //unschedule all the notifications, faster than calling the cancelAllNotifications func
        //UIApplication.shared.scheduledLocalNotifications = nil

        let cells = tableView.visibleCells
        if !cells.isEmpty {
            for i in 0..<cells.count {
                if alarmModel.alarms[i].enabled {
                    (cells[i].accessoryView as! UISwitch).setOn(true, animated: false)
                    cells[i].backgroundColor = UIColor.white
                    cells[i].textLabel?.alpha = 1.0
                    cells[i].detailTextLabel?.alpha = 1.0
                }
                else {
                    (cells[i].accessoryView as! UISwitch).setOn(false, animated: false)
                    cells[i].backgroundColor = UIColor.groupTableViewBackground
                    cells[i].textLabel?.alpha = 0.5
                    cells[i].detailTextLabel?.alpha = 0.5
                }
            }
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    // MARK: - Table view data source

    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 90
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        // Return the number of sections.
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // Return the number of rows in the section.
        if alarmModel.count == 0 {
            tableView.separatorStyle = UITableViewCellSeparatorStyle.none
        }
        else {
            tableView.separatorStyle = UITableViewCellSeparatorStyle.singleLine
        }
        return alarmModel.count
    }


    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if isEditing {
            performSegue(withIdentifier: Id.editSegueIdentifier, sender: SegueInfo(curCellIndex: indexPath.row, isEditMode: true, label: alarmModel.alarms[indexPath.row].label, mediaLabel: alarmModel.alarms[indexPath.row].mediaLabel, mediaID: alarmModel.alarms[indexPath.row].mediaID, repeatWeekdays: alarmModel.alarms[indexPath.row].repeatWeekdays, enabled: alarmModel.alarms[indexPath.row].enabled, dateTime: alarmModel.alarms[indexPath.row].date))
        }
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCell(withIdentifier: Id.alarmCellIdentifier)
        if (cell == nil) {
            cell = UITableViewCell(style: UITableViewCellStyle.subtitle, reuseIdentifier: Id.alarmCellIdentifier)
        }

        //cell text
        cell!.selectionStyle = .none
        cell!.tag = indexPath.row
        let alarm: Alarm = alarmModel.alarms[indexPath.row]
        let amAttr: [String : Any] = [NSFontAttributeName : UIFont.systemFont(ofSize: 20.0)]
        let str = NSMutableAttributedString(string: alarm.formattedTime, attributes: amAttr)
        let timeAttr: [String : Any] = [NSFontAttributeName : UIFont.systemFont(ofSize: 45.0)]
        str.addAttributes(timeAttr, range: NSMakeRange(0, str.length-2))
        cell!.textLabel?.attributedText = str
        cell!.detailTextLabel?.text = alarm.label
        //append switch button
        let sw = UISwitch(frame: CGRect())
        sw.transform = CGAffineTransform(scaleX: 0.9, y: 0.9);

        //tag is used to indicate which row had been touched
        sw.tag = indexPath.row
        sw.addTarget(self, action: #selector(MainAlarmViewController.switchTapped(_:)), for: UIControlEvents.touchUpInside)
        if alarm.enabled {
            sw.setOn(true, animated: false)
        }
        cell!.accessoryView = sw

        //delete empty seperator line
        tableView.tableFooterView = UIView(frame: CGRect.zero)

        return cell!
    }

    @IBAction func switchTapped(_ sender: UISwitch) {
        let index = sender.tag
        alarmModel.alarms[index].enabled = sender.isOn
        if sender.isOn {
            print("switch on")
            sender.superview?.backgroundColor = UIColor.white
            alarmScheduler.setNotificationWithDate(alarmModel.alarms[index].date, onWeekdaysForNotify: alarmModel.alarms[index].repeatWeekdays, snoozeEnabled: alarmModel.alarms[index].snoozeEnabled, onSnooze: false, soundName: alarmModel.alarms[index].mediaLabel, index: index)
            let cells = tableView.visibleCells
            if !cells.isEmpty {
                cells[index].textLabel?.alpha = 1.0
                cells[index].detailTextLabel?.alpha = 1.0
            }
        }
        else {
            print("switch off")
            sender.superview?.backgroundColor = UIColor.groupTableViewBackground
            let cells = tableView.visibleCells
            if !cells.isEmpty {
                cells[index].textLabel?.alpha = 0.5
                cells[index].detailTextLabel?.alpha = 0.5
            }
            alarmScheduler.reSchedule()
        }
    }


    // Override to support editing the table view.
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            let index = indexPath.row
            alarmModel.alarms.remove(at: index)
            let cells = tableView.visibleCells
            for cell in cells {
                let sw = cell.accessoryView as! UISwitch
                //adjust saved index when row deleted
                if sw.tag > index {
                    sw.tag -= 1
                }
            }
            if alarmModel.count == 0 {
                self.navigationItem.leftBarButtonItem = nil
            }

            // Delete the row from the data source
            tableView.deleteRows(at: [indexPath], with: .fade)
            alarmScheduler.reSchedule()
        }   
    }

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destinationViewController.
        // Pass the selected object to the new view controller.
        let dist = segue.destination as! UINavigationController
        let addEditController = dist.topViewController as! AlarmAddEditViewController
        if segue.identifier == Id.addSegueIdentifier {
            addEditController.navigationItem.title = "Add Alarm"
            addEditController.segueInfo = SegueInfo(curCellIndex: alarmModel.count, isEditMode: false, label: "Alarm", mediaLabel: "bell", mediaID: "", repeatWeekdays: [], enabled: false, dateTime: Date())
        }
        else if segue.identifier == Id.editSegueIdentifier {
            addEditController.navigationItem.title = "Edit Alarm"
            addEditController.segueInfo = sender as! SegueInfo
        }
    }

    @IBAction func unwindFromAddEditAlarmView(_ segue: UIStoryboardSegue) {
        isEditing = false
    }

}
Bista
  • 7,869
  • 3
  • 27
  • 55
Iurii Ushakov
  • 61
  • 1
  • 9
  • Do you mean to say the app crashes when you touch the cell or when you interact with the switch of that cell? – Rakshith Nandish Apr 15 '17 at 11:41
  • App crashes when I interact with the switch of that cell – Iurii Ushakov Apr 15 '17 at 11:42
  • 1
    Don't use cell tags; as you have found they cause problems when rows are reordered or deleted. See http://stackoverflow.com/questions/28659845/swift-how-to-get-the-indexpath-row-when-a-button-in-a-cell-is-tapped/38941510#38941510 for a better approach – Paulw11 Apr 15 '17 at 12:10

1 Answers1

1

When you get a runtime error, you should include the line that throws the error as part of your question.

Tags are a pretty fragile way to figure out what cell was tapped. (Tags are a pretty fragile way to do anything. There's almost always a better way to find views or figure out information about a view than using tags.)

I created a simple extension to UITableView that lets you ask the table view for the IndexPath of any view in a cell. It's easy to write a button IBAction that uses the extension to figure out which cell was tapped. I suggest reworking your code to use this approach

The extension:

public extension UITableView {

  /**
  This method returns the indexPath of the cell that contains the specified view
   - Parameter view: The view to find.
   - Returns: The indexPath of the cell containing the view, or nil if it can't be found
  */
    func indexPathForView(_ view: UIView) -> IndexPath? {
        let bounds = view.bounds
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let viewCenter = self.convert(center, from: view)
        let indexPath = self.indexPathForRow(at: viewCenter)
        return indexPath
    }
}

And the IBAction that uses it:

@IBAction func buttonTapped(_ button: UIButton) {
  if let indexPath = self.tableView.indexPathForView(button) {
    print("Button tapped at indexPath \(indexPath)")
  }
  else {
    print("Button indexPath not found")
  }
}

The whole project is available on Github:

TableViewExtension project on GitHub

I don't know why Apple doesn't build something like the indexPathForView function into UITableView. It seems like a universally useful function.

Duncan C
  • 128,072
  • 22
  • 173
  • 272