可重复使用的单元格和数组的问题
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
中。这似乎是一个普遍有用的功能。
【讨论】:
以上是关于可重复使用的单元格和数组的问题的主要内容,如果未能解决你的问题,请参考以下文章
具有自定义单元格和多节数组数据的 Swift UITableView