可重复使用的单元格和数组的问题

Posted

技术标签:

【中文标题】可重复使用的单元格和数组的问题【英文标题】:Trouble with reusable cells and array 【发布时间】:2017-04-15 11:31:01 【问题描述】:

我有闹钟应用程序。用户可以添加警报。如果我添加太多警报并向下滚动,我会看到

如果我尝试使用UISwitch(仅限按钮单元格),我将导致应用程序崩溃并出现错误:

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

我认为是数组里面的问题。如何解决?

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
    


【问题讨论】:

您的意思是说当您触摸单元格或与该单元格的开关交互时应用程序崩溃? 当我与该单元格的开关交互时应用程序崩溃 不要使用单元格标签;正如您发现的那样,当重新排序或删除行时,它们会导致问题。请参阅***.com/questions/28659845/… 以获得更好的方法 【参考方案1】:

当您遇到运行时错误时,您应该在问题中包含引发错误的行。

标签是一种非常脆弱的方式来确定哪个单元格被窃听了。 (标签是一种非常脆弱的方式来做任何事情。几乎总是有比使用标签更好的方法来查找视图或找出有关视图的信息。)

我为 UITableView 创建了一个简单的扩展,它允许您向表格视图询问单元格中任何视图的 IndexPath。编写一个按钮 IBAction 很容易,它使用扩展来确定哪个单元格被点击了。我建议修改您的代码以使用这种方法

扩展名:

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
    

以及使用它的 IBAction:

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

整个项目都可以在 Github 上找到:

TableViewExtension project on GitHub

我不知道为什么 Apple 不将类似 indexPathForView 的功能构建到 UITableView 中。这似乎是一个普遍有用的功能。

【讨论】:

以上是关于可重复使用的单元格和数组的问题的主要内容,如果未能解决你的问题,请参考以下文章

MATLAB 中的单元格和数组的连接和索引有何不同?

具有自定义单元格和多节数组数据的 Swift UITableView

EditingStyle - 删除数组中的单元格和项目

添加单元格和使用 UITableView 滚动时出现图形故障

如何获取特定匹配值的起始单元格和结束单元格地址?

如何在 UITableView 中为不同的单元格和标题设置不同的随机颜色?