You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过键盘工具栏按钮切换UITableViewCell中UITextField的第一响应者

问题描述

开发iOS购物清单应用时,商品录入页面的UITableView根据商品类型动态展示不同原型单元格,部分单元格包含UITextField用于输入。为键盘添加了带返回、前进、完成按钮的工具栏,完成按钮可正常隐藏键盘,但前进/返回按钮无法正确切换UITextField成为firstResponder:光标未定位到目标输入框,切换不同原型单元格的输入框时键盘消失,返回功能完全失效。


原实现代码

全局通知定义

let keyBoardBarBackNotification = Notification.Name("keyBoardBarBackNotification")
let keyBoardBarNextNotification = Notification.Name("keyBoardBarNextNotification")

UIViewController扩展(键盘工具栏)

func setupKeyboardBar() -> UIToolbar {
    let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 50))
    let leftButton = UIBarButtonItem(image: UIImage(systemName: "chevron.left"), style: .plain, target: self, action: #selector(leftButtonTapped))
    leftButton.tintColor = UIColor.systemBlue
    let nextButton = UIBarButtonItem(image: UIImage(systemName: "chevron.right"), style: .plain, target: self, action: #selector(nextButtonTapped))
    nextButton.tintColor = UIColor.systemBlue
    let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
    let fixSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
    let doneButton = UIBarButtonItem(image: UIImage(systemName: "keyboard.chevron.compact.down"), style: .plain, target: self, action: #selector(doneButtonTapped))
    doneButton.tintColor = UIColor.darkGray
    toolbar.setItems([fixSpace, leftButton, fixSpace, nextButton, flexSpace, doneButton], animated: true)
    toolbar.sizeToFit()
    return toolbar
}

@objc func leftButtonTapped() {
    view.endEditing(true)
    NotificationCenter.default.post(Notification(name: keyBoardBarBackNotification))
}

@objc func nextButtonTapped() {
    view.endEditing(true)
    NotificationCenter.default.post(Notification(name: keyBoardBarNextNotification))
}

@objc func doneButtonTapped() {
    view.endEditing(true)
}

视图控制器核心逻辑

class AddPositionVC: UIViewController {

@IBOutlet weak var menue: UITableView!


override func viewDidLoad() {
    super.viewDidLoad()
    self.menue.delegate = self
    self.menue.dataSource = self
    self.menue.separatorStyle = .none
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(handleBackButtonPressed), name: keyBoardBarBackNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(handleNextButtonPressed), name: keyBoardBarNextNotification, object: nil)
}


enum TableCellType: String {
    case product = "Product:"
    case brand = "Brand:"
    case quantity = "Quantity:"
    case price = "Price:"
    case shop = "Shop:"
    // ...
}

var actualField = TableCellType.product  // 当前成为firstResponder的字段

// 定义要显示的字段数组
var menueList: Array<TableCellType> = [.product, .brand, .quantity, .shop]

// 存储字段对应的IndexPath
var tableViewIndex = Dictionary<TableCellType, IndexPath>()

@objc func handleKeyboardDidShow(notification: NSNotification) {
    guard let endframeKeyboard = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey]
                as? CGRect else { return }
    let insets = UIEdgeInsets( top: 0, left: 0, bottom: endframeKeyboard.size.height - 60, right: 0 )
    self.menue.contentInset = insets
    self.menue.scrollIndicatorInsets = insets
    self.scrollToMenuezeile(self.actualField)
    self.view.layoutIfNeeded()
}

@objc func handleKeyboardWillHide()  {
    self.menue.contentInset = .zero
    self.view.layoutIfNeeded()
}

@objc func handleBackButtonPressed() {
    switchActiveField(self.actualField, back: true)
}

@objc func handleNextButtonPressed() {
    switchActiveField(self.actualField, back: false)
}

// 定义下一个要成为firstResponder的字段
func switchActiveField(_ art: TableCellType, back bck: Bool) {
    switch art {
    case .brand:
        self.actualField = bck ? .product : .quantity
    case .quantity:
        self.actualField = bck ? .brand : .shop
    case .price:
        self.actualField = bck ? .quantity : .shop
    case .product:
        self.actualField = bck ? .shop : .brand
    case .shop:
        self.actualField = bck ? .price : .product
    // ....
    }
    if let index = self.tableViewIndex[self.actualField] {
            self.menue.reloadRows(at: [index], with: .automatic)
    }
}

UITableView代理实现

extension AddPositionVC: UITableViewDelegate, UITableViewDataSource {
    
    func scrollToMenuezeile(_ art: TableCellType) {
        if let index = self.tableViewIndex[art] {
            self.menue.scrollToRow(at: index, at: .bottom, animated: false)
        }
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return menueList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let tableCellType = self.menueList[indexPath.row]
        self.tableViewIndex[tableCellType] = indexPath
        switch tableCellType {
        case .product, .brand, .shop:
            let cell = tableView.dequeueReusableCell(withIdentifier: "LabelTextFieldCell", for: indexPath) as! LabelTextFieldCell
            cell.item.text = tableCellType.rawValue
            cell.itemInput.inputAccessoryView = self.setupKeyboardBar()
            cell.itemInput.text = "" // 此处会清空输入内容
            if self.actualField == tableCellType {
                cell.itemInput.becomeFirstResponder()
            }
            return cell
        case .quantity, .price:
            let cell = tableView.dequeueReusableCell(withIdentifier: "QuantityPriceCell", for: indexPath) as! QuantityPriceCell
            cell.quantity.inputAccessoryView = self.setupKeyboardBar()
            cell.quantity.text = "" // 此处会清空输入内容
            cell.price.inputAccessoryView = self.setupKeyboardBar()
            cell.price.text = "" // 此处会清空输入内容
            if self.actualField == .price {
                cell.price.becomeFirstResponder()
            } else if self.actualField == .quantity {
                cell.quantity.becomeFirstResponder()
            }
            return cell
        }
    }
}

//*********************************************
// MARK: - 表格单元格定义
//*********************************************

class LabelTextFieldCell: UITableViewCell, UITextFieldDelegate {
    
    override func awakeFromNib() {
        super.awakeFromNib()
        itemInput.delegate = self
    }
    
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }
    
    func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
        self.itemInput.resignFirstResponder() // 重复调用resign
    }
    
    @IBOutlet weak var item: UILabel!
    @IBOutlet weak var itemInput: UITextField!
}

class QuantityPriceCell: UITableViewCell, UITextFieldDelegate {
    
    override func awakeFromNib() {
        super.awakeFromNib()
        self.quantity.delegate = self
        self.price.delegate = self
    }
    
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }
    
    func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
        textField.resignFirstResponder() // 重复调用resign
    }
    
    @IBOutlet weak var quantity: UITextField!
    @IBOutlet weak var price: UITextField!
}

解决方案

问题根源分析

  1. 切换按钮中提前调用view.endEditing(true),直接收起键盘,后续试图让新输入框成为firstResponder时失效
  2. switchActiveField中逻辑错误:menueList里没有.price字段,但切换.shop的返回时指向了.price,导致找不到对应的IndexPath,返回功能完全失效
  3. 依赖reloadRows触发cellForRow中的becomeFirstResponder,但reload是异步操作,时机无法保证,导致光标无法正确定位
  4. cellForRow中每次都设置输入框文本为空,丢失用户输入内容
  5. 单元格的textFieldDidEndEditing中重复调用resignFirstResponder,干扰键盘状态

修复步骤及代码修改

1. 移除切换按钮中的提前收起键盘操作

修改UIViewController扩展中的按钮点击方法:

@objc func leftButtonTapped() {
    // 移除 view.endEditing(true),避免提前收起键盘
    NotificationCenter.default.post(Notification(name: keyBoardBarBackNotification))
}

@objc func nextButtonTapped() {
    // 移除 view.endEditing(true),避免提前收起键盘
    NotificationCenter.default.post(Notification(name: keyBoardBarNextNotification))
}

2. 修复字段切换逻辑错误

由于menueList中没有.price,调整switchActiveField中.shop的返回逻辑,并新增直接激活目标输入框的方法:

func switchActiveField(_ art: TableCellType, back bck: Bool) {
    switch art {
    case .brand:
        self.actualField = bck ? .product : .quantity
    case .quantity:
        self.actualField = bck ? .brand : .shop
    case .price:
        self.actualField = bck ? .quantity : .shop
    case .product:
        self.actualField = bck ? .shop : .brand
    case .shop:
        // 原逻辑指向.price,但menueList中无此字段,改为指向quantity
        self.actualField = bck ? .quantity : .product
    // ....
    }
    // 直接获取目标cell并设置firstResponder,不再依赖reloadRows
    activateTargetField()
}

// 新增方法:激活目标输入框
private func activateTargetField() {
    guard let indexPath = tableViewIndex[actualField] else { return }
    // 滚动到目标行
    menue.scrollToRow(at: indexPath, at: .bottom, animated: true)
    // 获取目标cell
    guard let cell = menue.cellForRow(at: indexPath) else {
        // 如果cell尚未加载,先刷新再尝试
        menue.reloadRows(at: [indexPath], with: .automatic)
        DispatchQueue.main.async { [weak self] in
            self?.activateTargetField()
        }
        return
    }
    // 根据字段类型激活对应的输入框
    switch actualField {
    case .product, .brand, .shop:
        let textFieldCell = cell as! LabelTextFieldCell
        textFieldCell.itemInput.becomeFirstResponder()
    case .quantity:
        let quantityPriceCell = cell as! QuantityPriceCell
        quantityPriceCell.quantity.becomeFirstResponder()
    case .price:
        let quantityPriceCell = cell as! QuantityPriceCell
        quantityPriceCell.price.becomeFirstResponder()
    }
}

3. 保留用户输入内容

首先在AddPositionVC中添加存储输入内容的属性:

// 存储各字段的输入内容
var fieldValues: [TableCellType: String] = [:]

然后修改cellForRow中的文本设置,并添加输入值存储逻辑:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let tableCellType = self.menueList[indexPath.row]
    self.tableViewIndex[tableCellType] = indexPath
    switch tableCellType {
    case .product, .brand, .shop:
        let cell = tableView.dequeueReusableCell(withIdentifier: "LabelTextFieldCell", for: indexPath) as! LabelTextFieldCell
        cell.item.text = tableCellType.rawValue
        cell.itemInput.inputAccessoryView = self.setupKeyboardBar()
        // 从fieldValues中读取已输入的内容,而非清空
        cell.itemInput.text = fieldValues[tableCellType] ?? ""
        // 设置代理,监听输入变化
        cell.itemInput.delegate = self
        // 绑定字段类型到输入框,方便后续存储值
        cell.itemInput.tag = tableCellType.rawValue.hashValue
        if self.actualField == tableCellType {
            DispatchQueue.main.async {
                cell.itemInput.becomeFirstResponder()
            }
        }
        return cell
    case .quantity, .price:
        let cell = tableView.dequeueReusableCell(withIdentifier: "QuantityPriceCell", for: indexPath) as! QuantityPriceCell
        cell.quantity.inputAccessoryView = self.setupKeyboardBar()
        cell.price.inputAccessoryView = self.setupKeyboardBar()
        // 读取已输入内容
        cell.quantity.text = fieldValues[.quantity] ?? ""
        cell.price.text = fieldValues[.price] ?? ""
        // 设置代理和tag
        cell.quantity.delegate = self
        cell.quantity.tag = TableCellType.quantity.rawValue.hashValue
        cell.price.delegate = self
        cell.price.tag = TableCellType.price.rawValue.hashValue
        if self.actualField == .price {
            DispatchQueue.main.async {
                cell.price.becomeFirstResponder()
            }
        } else if self.actualField == .quantity {
            DispatchQueue.main.async {
                cell.quantity.becomeFirstResponder()
            }
        }
        return cell
    }
}

让AddPositionVC实现UITextFieldDelegate,存储输入值:

extension AddPositionVC: UITextFieldDelegate {
    func textFieldDidEndEditing(_ textField: UITextField) {
        // 通过tag找到对应的字段类型
        guard let type = TableCellType.allCases.first(where: { $0.rawValue.hashValue == textField.tag }) else { return }
        fieldValues[type] = textField.text ?? ""
    }
}

4. 移除单元格中重复的resignFirstResponder调用

修改LabelTextFieldCell:

func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
    // 移除 self.itemInput.resignFirstResponder(),避免重复调用
}

修改QuantityPriceCell:

func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
    // 移除 textField.resignFirstResponder(),避免重复调用
}

额外优化建议

  • 可以移除全局通知,直接在按钮点击时调用AddPositionVC中的切换方法,减少通知带来的耦合
  • 对TableCellType的hashValue绑定tag的方式,可以改为自定义rawValue为Int类型,更可靠
  • 在activateTargetField中添加延迟确保cell完全加载,避免异步问题

内容的提问来源于stack exchange,提问作者carlson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 23:20:38