iOS开发:点击UIStepper操作itemArray触发数组越界错误排查
iOS开发数组越界问题排查:UIStepper触发协议方法时访问itemArray报错
问题描述
在GearComponentViewController的tableView中,自定义GearComponentTableViewCell包含UIStepper组件用于控制数量标签,ViewController遵循自定义协议Stepper。当点击Stepper的加减按钮,取消注释itemArray[userindexPath].quantity += 1或itemArray[userindexPath].quantity -= 1时,触发Thread 1: Fatal error: Index out of range错误,但此时itemArray不为空,已有Cell正常显示。
相关代码
自定义协议Stepper
protocol Stepper { func stepperWasPressed(didIncrease: Bool, namePassed: String, userindexPath: Int) }
GearComponentTableViewCell代码
class GearComponentTableViewCell: UITableViewCell { var mainVC = GearComponentViewController() var tableViewCellPosition: Int! = nil // Image @IBOutlet weak var itemImage: UIImageView! // Name @IBOutlet weak var itemName: UILabel! // Weight @IBOutlet weak var itemWeight1: UILabel! @IBOutlet weak var itemWeight2: UILabel! // Quanity @IBOutlet weak var itemQuanity: UILabel! @IBAction func stepperPressed (_ sender: UIStepper!){ if (sender.value == 1) { print("up and item: \(itemName.text!)"); sender.value = 0 mainVC.stepperWasPressed(didIncrease: true, namePassed: itemName.text!, userindexPath: tableViewCellPosition) } else if (sender.value == -1) { print("down and item: \(itemName.text!)"); sender.value = 0 mainVC.stepperWasPressed(didIncrease: false, namePassed: itemName.text!, userindexPath: tableViewCellPosition) } } // Notes @IBOutlet weak var itemNotes: UILabel! }
GearComponentViewController的Stepper扩展
extension GearComponentViewController: Stepper { func refreshTableViewCell() { print("arrayPosition: \(arrayPosition)") // gearTableView.reloadData() } func stepperWasPressed(didIncrease: Bool, namePassed: String, userindexPath: Int) { if didIncrease { arrayPosition = userindexPath print("arrayPosition: \(arrayPosition)") // itemArray[userindexPath].quantity += 1 print("userindexPath: \(userindexPath) -- namePassed: \(namePassed) -- didIncrease: \(didIncrease)") print("increase selected") refreshTableViewCell() }else { arrayPosition = userindexPath print("arrayPosition: \(arrayPosition)") // itemArray[userindexPath].quantity -= 1 print("userindexPath: \(userindexPath) -- namePassed: \(namePassed) -- didIncrease: \(didIncrease)") print("decrease selected") refreshTableViewCell() } } }
完整GearComponentViewController代码
import UIKit class GearComponentViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { var arrayPosition: Int = 0 // Data Sources var itemArray: [GearItem] = [] var totalWeight1: Int = 0 var totalWeight2: Int = 0 var totalItems: Int = 0 // Weight Label Outlets @IBOutlet weak var weight1LabelOutlet: UILabel! @IBOutlet weak var weight2LabelOutlet: UILabel! // Item Count Label Outlet @IBOutlet weak var totalCountLabelOutlet: UILabel! // TableView Outlet @IBOutlet weak var gearTableView: UITableView! // Bar Button Item @IBAction func addItemPressed(_ sender: Any) { presentAlert() } // View Did Load override func viewDidLoad() { super.viewDidLoad() gearTableView.dataSource = self gearTableView.delegate = self print("Gear Component View Controller successfully loaded.") updateUI() } //MARK: - UI Updater func updateUI() { addTotalWeight() addAllItems() } // Adding Total Weight func addTotalWeight() { var addedWeight1: Int = 0 var addedWeight2: Int = 0 for item in itemArray { addedWeight1 += item.weight1 * item.quantity addedWeight2 += item.weight2 * item.quantity } totalWeight1 = addedWeight1 totalWeight2 = addedWeight2 weight1LabelOutlet.text = String("\(totalWeight1)") weight2LabelOutlet.text = String("\(totalWeight2)") } // Item Count func addAllItems() { var addedValue: Int = 0 for item in itemArray { addedValue += item.quantity } totalItems = addedValue totalCountLabelOutlet.text = String("\(totalItems)") print("Total items: \(totalItems)") } //MARK: - Alert func presentAlert() { let alertController = UIAlertController(title: "Add Item", message: "Enter item info here", preferredStyle: .alert) // AlertController Textfields alertController.addTextField { (textField) in textField.placeholder = "Name" } alertController.addTextField { (textField) in textField.placeholder = "Weight 1" textField.keyboardType = .decimalPad } alertController.addTextField { (textField) in textField.placeholder = "Weight 2" textField.keyboardType = .decimalPad } alertController.addTextField { (textField) in textField.placeholder = "Quanity" textField.keyboardType = .decimalPad } alertController.addTextField { (textField) in textField.placeholder = "Notes" } // AlertController Actions let continueAction = UIAlertAction(title: "Add", style: .default) { [self, weak alertController] _ in guard let textFields = alertController?.textFields else { return } if let userItemName = textFields[0].text, let userItemWeight1 = textFields[1].text, let userItemWeight2 = textFields[2].text, let userItemQuanity = textFields[3].text, let userItemNotes = textFields[4].text { print("Name: \(userItemName)") print("Item Weight 1: \(userItemWeight1)") print("Item Weight 2: \(userItemWeight2)") print("Item Quantity: \(userItemQuanity)") print("Item Notes: \(userItemNotes)") let userSubmittedItem = GearItem(itemName: userItemName, itemImage: UIImage(systemName: "photo.on.rectangle.angled")!, itemWeight1: Int(userItemWeight1) ?? 0, itemWeight2: Int(userItemWeight2) ?? 0, itemQuantity: Int(userItemQuanity) ?? 1, itemNotes: userItemNotes ?? "", creationPosition: itemArray.count + 1) self.itemArray.append(userSubmittedItem) gearTableView.reloadData() updateUI() } } let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) cancelAction.setValue(UIColor.red, forKey: "titleTextColor") alertController.addAction(continueAction) alertController.addAction(cancelAction) self.present(alertController, animated: true) } //MARK: - TableView Protocol Functions func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return itemArray.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = gearTableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! GearComponentTableViewCell cell.itemName.text = itemArray[indexPath.row].name cell.itemImage.image = itemArray[indexPath.row].image cell.itemWeight1.text = String("\(itemArray[indexPath.row].weight1)") cell.itemWeight2.text = String("\(itemArray[indexPath.row].weight2)") cell.itemQuanity.text = String("\(itemArray[indexPath.row].quantity)") cell.itemNotes.text = itemArray[indexPath.row].notes cell.tableViewCellPosition = indexPath.row return cell } //MARK: - Swipeable TableViewCell private func deleteItem(_ indexPath: IndexPath) { itemArray.remove(at: indexPath.row) gearTableView.reloadData() updateUI() print("Item Deleted") } func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let action = UIContextualAction(style: .normal, title: "Delete") { [weak self] (action, view, completionHandler) in self?.deleteItem(indexPath) completionHandler(true) } action.backgroundColor = .systemRed return UISwipeActionsConfiguration(actions: [action]) } } extension GearComponentViewController: Stepper { func refreshTableViewCell() { print("arrayPosition: \(arrayPosition)") // itemArray[arrayPosition].quantity += 1 // gearTableView.reloadData() } func stepperWasPressed(didIncrease: Bool, namePassed: String, userindexPath: Int) { if didIncrease { arrayPosition = userindexPath print("arrayPosition: \(arrayPosition)") // itemArray[userindexPath].quantity += 1 print("userindexPath: \(userindexPath) -- namePassed: \(namePassed) -- didIncrease: \(didIncrease)") print("increase selected") refreshTableViewCell() }else { arrayPosition = userindexPath print("arrayPosition: \(arrayPosition)") // itemArray[userindexPath].quantity -= 1 print("userindexPath: \(userindexPath) -- namePassed: \(namePassed) -- didIncrease: \(didIncrease)") print("decrease selected") refreshTableViewCell() } } }
问题原因与解决方案
核心问题
- 错误的ViewController实例引用:
GearComponentTableViewCell中直接创建var mainVC = GearComponentViewController(),这是一个全新的空实例,而非当前显示的ViewController。这个新实例的itemArray为空,调用stepperWasPressed时访问数组必然越界。 - 索引失效风险:即使解决实例问题,删除Cell后,已复用Cell保存的
tableViewCellPosition可能指向已被移除的索引,后续点击Stepper仍会触发越界。
修复步骤
步骤1:用代理模式替换直接创建实例
修改GearComponentTableViewCell,移除mainVC属性,改为弱引用代理:
class GearComponentTableViewCell: UITableViewCell { weak var delegate: Stepper? var tableViewCellPosition: Int! = nil // ... 其他属性和IBOutlet不变 @IBAction func stepperPressed (_ sender: UIStepper!){ guard let position = tableViewCellPosition, let delegate = delegate else { return } if sender.value == 1 { print("up and item: \(itemName.text!)"); sender.value = 0 delegate.stepperWasPressed(didIncrease: true, namePassed: itemName.text ?? "", userindexPath: position) } else if sender.value == -1 { print("down and item: \(itemName.text!)"); sender.value = 0 delegate.stepperWasPressed(didIncrease: false, namePassed: itemName.text ?? "", userindexPath: position) } } // ... 其他代码不变 }
步骤2:在ViewController中设置Cell代理
在cellForRowAt方法中添加代理赋值:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = gearTableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! GearComponentTableViewCell // ... 其他赋值代码不变 cell.delegate = self cell.tableViewCellPosition = indexPath.row return cell }
步骤3:添加索引合法性校验
在stepperWasPressed方法中先校验索引有效性,同时避免数量减到负数:
func stepperWasPressed(didIncrease: Bool, namePassed: String, userindexPath: Int) { guard userindexPath >= 0 && userindexPath < itemArray.count else { print("Invalid index: \(userindexPath)") return } if didIncrease { itemArray[userindexPath].quantity += 1 }else { if itemArray[userindexPath].quantity > 0 { itemArray[userindexPath].quantity -= 1 } } print("userindexPath: \(userindexPath) -- namePassed: \(namePassed) -- didIncrease: \(didIncrease)") gearTableView.reloadRows(at: [IndexPath(row: userindexPath, section: 0)], with: .automatic) updateUI() }
步骤4:修正拼写错误
代码中quanity为拼写错误,统一改为正确的quantity,确保模型与代码属性名一致。
内容的提问来源于stack exchange,提问作者slimeforest
相关产品推荐
相关产品推荐

