ios 如何在UITableViewCells中的UITextFields之间使用键盘工具栏上的按钮更改firstResponder

fhity93d  于 2022-12-24  发布在  iOS
关注(0)|答案(1)|浏览(115)

我想这是一个低的头脑,我挣扎,但不幸的是,我所有的搜索在这个论坛和其他来源没有给予我一个胶水呢。
我正在为iOS创建一个购物清单应用程序。在用于输入购物清单位置的Viewcontroller中,我只显示相关的输入字段,这取决于要放在购物清单上的商品种类。
因此,我用不同的原型单元设置了一个tableView,其中一些单元包含UITextFields来处理这种动态设置。
我已经定义了一个键盘工具栏,包含一个按钮在右边隐藏键盘(这工作)和两个按钮(“下一个”和“返回”)在左边跳转到下一个相应的前一个输入字段,这应该成为第一响应者,光标设置在这个字段并显示键盘。

不幸的是,这种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)
}

}
在viewController中,我设置了键盘处理例程和一个例程“switchActiveField”来确定下一个应该成为firstResponder的实际字段:

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  // field that becomes firstResponder

// Arrray, defining the fields to be diplayed
var menueList: Array<TableCellType> = [.product, .brand, .quantity, .shop
]

// Array with IndexPath of displayed fields
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)
}

// Definition, which field should become next 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)
    }
}

}
tableView的扩展名为:

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 = "" // respective Input
            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 = "" // respective Input
            cell.price.inputAccessoryView = self.setupKeyboardBar()
            cell.price.text = "" // respective Input
            if self.actualField == .price {
                cell.price.becomeFirstResponder()
            } else if self.actualField == .quantity {
                cell.quantity.becomeFirstResponder()
            }
            return cell
        }
    }
}

//*********************************************
// MARK: - tableViewCells
//*********************************************

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()
    }
    
    @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()
    }
    
    @IBOutlet weak var quantity: UITextField!
    @IBOutlet weak var price: UITextField!
    
}

谢谢你的支持。

wqlqzqxt

wqlqzqxt1#

有各种各样的方法可以实现这一点......事实上,很容易找到具有许多特性的开源第三方库--只需搜索(Google或任何地方)swift ios form builder
但是,如果你想自己做,基本的想法是:

  • 将文本字段添加到数组
  • 添加类级别的var/属性,如var activeField: UITextField?
  • 对于textFieldDidBeginEditing上的每个字段:
  • 自身活动字段=文本字段
  • 当用户点击“下一步”按钮时:
guard let aField = self.activeField,
        let idx = self.textFields.firstIndex(of: aField)
  else { return }
  if idx == self.textFields.count - 1 {
      // "wrap around" to first field
      textFields.first?.becomeFirstResponder()
  } else {
      // "move to" next field
      textFields[idx + 1].becomeFirstResponder()
  }

如果你所有的字段都在屏幕上,那就很简单了。
如果它们不能垂直放置(特别是当键盘显示时),如果它们都在滚动视图中,同样,非常直接。
将它们放入tableView的单元格中会变得很复杂,原因有以下几个:

  • 单元格不一定按顺序生成,因此您必须编写更多代码,以便按正确的顺序从一个字段移动到另一个字段
  • 如果您的单元格数超出了屏幕的容纳范围,则“下一个字段”可能不存在!例如,假设您有8行...只能容纳5行...您正在编辑最后一行中的字段,然后点击“下一步”按钮。您想要移动到第0行中的字段,但在您向上滚动到顶部之前,第0行将不存在。

要添加重复的相似但不同的“行”,我们不需要使用表格视图。
例如,如果我们有一个UIStackView.axis = .vertical

for i in 1...10 {
    let label = UILabel()
    label.text = "Row \(i)"
    stackView.addArrangedSubview(label)
}

我们现在添加了10个单标签“细胞”。
因此,对于您的任务,我们可以编写以下函数,而不是在LabelTextFieldCell中使用表视图:

func buildLabelTextFieldView(labelText str: String) -> UIView {
    let aView = UIView()
    
    let label: UILabel = {
        let v = UILabel()
        v.font = .systemFont(ofSize: 15.0, weight: .light)
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    let field: UITextField = {
        let v = UITextField()
        v.borderStyle = .bezel
        v.font = .systemFont(ofSize: 15.0, weight: .light)
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    label.text = str
    
    self.textFields.append(field)
    
    aView.addSubview(label)
    aView.addSubview(field)
    NSLayoutConstraint.activate([
        label.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 0.0),
        label.firstBaselineAnchor.constraint(equalTo: field.firstBaselineAnchor),
        field.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8.0),
        field.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: 0.0),
        field.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
        field.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
    ])
    return aView
}

以及类似的(但稍微复杂一些):

func buildQuantityPriceView() -> UIView {
    let aView = UIView()
    ...
    return aView
}

然后类似于cellForRowAt使用它:

for i in 0..<menueList.count {
        let tableCellType = menueList[i]
        
        var rowView: UIView!
        
        switch tableCellType {
            
        case .product, .brand, .shop:
            rowView = buildLabelTextFieldView(labelText: tableCellType.rawValue)

        case .quantity, .price:
            rowView = buildQuantityPriceView()
            
        }
    
        stackView.addArrangedSubview(rowView)
    }

如果我们将stackView添加到scrollView,我们就有了一个可滚动的“Form”。
下面是一个完整的示例,您可以尝试(没有@IBOutlet@IBAction连接......只需要将一个空白视图控制器的类设置为FormVC):

class FormVC: UIViewController, UITextFieldDelegate {
    
    var textFields: [UITextField] = []
    
    let scrollView = UIScrollView()
    
    var menueList: Array<TableCellType> = [.product, .brand, .quantity, .shop]
    
    lazy var kbToolBar: 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
    }()
    
    var activeField: UITextField?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let stackView = UIStackView()
        
        stackView.axis = .vertical
        stackView.spacing = 32
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(stackView)
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(scrollView)
        
        let g = view.safeAreaLayoutGuide
        let cg = scrollView.contentLayoutGuide
        let fg = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            
            scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 16.0),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
            scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -16.0),
            
            stackView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 8.0),
            stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 8.0),
            stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -8.0),
            stackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -8.0),
            
            stackView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -16.0),
            
        ])
        
        for i in 0..<menueList.count {
            let tableCellType = menueList[i]
            
            var rowView: UIView!
            
            switch tableCellType {
                
            case .product, .brand, .shop:
                rowView = buildLabelTextFieldView(labelText: tableCellType.rawValue)

            case .quantity, .price:
                rowView = buildQuantityPriceView()
                
            }
        
            stackView.addArrangedSubview(rowView)
        }
        
        // we've added all the labels and fields
        //  and our textFields array contains all the fields in order
        
        // we want all the "first/left" labels to be equal widths
        guard let firstLabel = stackView.arrangedSubviews.first?.subviews.first as? UILabel
        else  {
            fatalError("We did something wrong in our setup!")
        }
        stackView.arrangedSubviews.forEach { v in
            // skip the first one
            if v != stackView.arrangedSubviews.first {
                if let thisLabel = v.subviews.first as? UILabel {
                    thisLabel.widthAnchor.constraint(equalTo: firstLabel.widthAnchor).isActive = true
                }
            }
        }
        
        // set inputAccessoryView and delegate on all the text fields
        textFields.forEach { v in
            v.inputAccessoryView = kbToolBar
            v.delegate = self
        }
        
        // prevent keyboard from hiding scroll view elements
        let notificationCenter = NotificationCenter.default
        notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
        
        // during dev, use "if true" and set some colors so we can see view framing
        if false {
            view.backgroundColor = .systemYellow
            scrollView.backgroundColor = .yellow
            stackView.layer.borderColor = UIColor.red.cgColor
            stackView.layer.borderWidth = 1
            stackView.arrangedSubviews.forEach { v in
                v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            }
        }
        
    }
    
    @objc func leftButtonTapped() {
        guard let aField = self.activeField,
              let idx = self.textFields.firstIndex(of: aField)
        else { return }
        if idx == 0 {
            textFields.last?.becomeFirstResponder()
        } else {
            textFields[idx - 1].becomeFirstResponder()
        }
    }
    
    @objc func nextButtonTapped() {
        guard let aField = self.activeField,
              let idx = self.textFields.firstIndex(of: aField)
        else { return }
        if idx == self.textFields.count - 1 {
            textFields.first?.becomeFirstResponder()
        } else {
            textFields[idx + 1].becomeFirstResponder()
        }
    }
    
    @objc func doneButtonTapped() {
        view.endEditing(true)
    }

    func textFieldDidBeginEditing(_ textField: UITextField) {
        self.activeField = textField
    }
    func textFieldDidEndEditing(_ textField: UITextField) {
        self.activeField = nil
    }

    @objc func adjustForKeyboard(notification: Notification) {
        guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
        
        let keyboardScreenEndFrame = keyboardValue.cgRectValue
        let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
        
        if notification.name == UIResponder.keyboardWillHideNotification {
            self.scrollView.contentInset = .zero
        } else {
            self.scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height - view.safeAreaInsets.bottom, right: 0)
        }
        
        self.scrollView.scrollIndicatorInsets = self.scrollView.contentInset
    }
}

我们将把“行视图”构建器函数放在扩展中,只是为了保持代码的独立性和可读性:

extension FormVC {
    func buildLabelTextFieldView(labelText str: String) -> UIView {
        let aView = UIView()
        
        let label: UILabel = {
            let v = UILabel()
            v.font = .systemFont(ofSize: 15.0, weight: .light)
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        let field: UITextField = {
            let v = UITextField()
            v.borderStyle = .bezel
            v.font = .systemFont(ofSize: 15.0, weight: .light)
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        label.text = str
        
        self.textFields.append(field)
        
        aView.addSubview(label)
        aView.addSubview(field)
        NSLayoutConstraint.activate([
            label.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 0.0),
            label.firstBaselineAnchor.constraint(equalTo: field.firstBaselineAnchor),
            field.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8.0),
            field.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: 0.0),
            field.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
            field.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
        ])
        return aView
    }
}

extension FormVC {
    func buildQuantityPriceView() -> UIView {
        let aView = UIView()
        
        let labelA: UILabel = {
            let v = UILabel()
            v.font = .systemFont(ofSize: 15.0, weight: .light)
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        let fieldA: UITextField = {
            let v = UITextField()
            v.borderStyle = .bezel
            v.font = .systemFont(ofSize: 15.0, weight: .light)
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        labelA.text = "Quantity:"
        
        self.textFields.append(fieldA)
        
        let labelB: UILabel = {
            let v = UILabel()
            v.font = .systemFont(ofSize: 15.0, weight: .light)
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        let fieldB: UITextField = {
            let v = UITextField()
            v.borderStyle = .bezel
            v.font = .systemFont(ofSize: 15.0, weight: .light)
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        labelB.text = "Price:"
        
        self.textFields.append(fieldB)
        
        aView.addSubview(labelA)
        aView.addSubview(fieldA)
        aView.addSubview(labelB)
        aView.addSubview(fieldB)
        NSLayoutConstraint.activate([
            labelA.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 0.0),
            labelA.firstBaselineAnchor.constraint(equalTo: fieldA.firstBaselineAnchor),
            fieldA.leadingAnchor.constraint(equalTo: labelA.trailingAnchor, constant: 8.0),
            fieldA.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
            fieldA.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
            labelB.leadingAnchor.constraint(equalTo: fieldA.trailingAnchor, constant: 8.0),
            labelB.firstBaselineAnchor.constraint(equalTo: fieldB.firstBaselineAnchor),
            fieldB.leadingAnchor.constraint(equalTo: labelB.trailingAnchor, constant: 8.0),
            fieldB.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
            fieldB.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
            fieldB.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: 0.0),
            
            // we want both fields to be equal widths
            fieldB.widthAnchor.constraint(equalTo: fieldA.widthAnchor),
        ])
        return aView
    }
}

运行时,它看起来像这样:

如果您添加更多的“行”-或者更简单地增加堆栈视图间距,如stackView.spacing = 100-您将看到当键盘显示时,它如何继续使用scrollView。
当然,你在评论中提到:"...更多输入字段(例如,使用日期选择器的日期等)",因此您需要编写新的“行构建器”函数,并向Next tap添加一些逻辑,以转到/来自选择器,而不是文本字段。
但是,你可能会发现这是一个有帮助的起点。

相关问题