[英]Trouble with reusable cells and array
I have Alarm Clock App. 我有闹钟应用程序。 User can add alarms.
用户可以添加警报。 If I add too much alarms and scroll down I will see that
如果我添加太多警报并向下滚动,我会看到
If i tried to use UISwitch
(only buttom cell) I will crash the app and get Error: 如果我尝试使用
UISwitch
(仅适用于buttom单元格),则会使应用程序崩溃并出现错误:
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
}
}
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. 我为UITableView创建了一个简单的扩展,使您可以向表视图询问单元格中任何视图的IndexPath。 It's easy to write a button IBAction that uses the extension to figure out which cell was tapped.
编写一个按钮IBAction很容易,该按钮使用扩展名来找出被点击的单元格。 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:
@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: 整个项目在Github上可用:
TableViewExtension project on GitHub GitHub上的TableViewExtension项目
I don't know why Apple doesn't build something like the indexPathForView
function into UITableView
. 我不知道为什么苹果没有在
UITableView
构建诸如indexPathForView
函数之类的东西。 It seems like a universally useful function. 它似乎是一个通用的功能。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.