I guess that it is a low brainer I'm struggling with, but unfortunately all my searches in this forum and other sources didn't give me a glue yet.
I'm creating a shopping list app for iOS. In the Viewcontroller for the entry of the shoppinglist positions I'm showing only the relevant entry fields depending on the kind of goods to be put on the shopping list.
Hence I have set up a tableView with different prototype cells and some of them contain UITextFields to handle this dynamic setup.
I have defined a toolbar for the keyboard containing one button at the right to hide the keyboard (which works) and two buttons ("next" & "back") on the left to jump to the next respectively previous input field, which should then become first responder, cursor set in this field and showing the keyboard.
Unfortunately this handing over of the firstResponder isn't working and the cursor is not set to the next/previous input field and sometimes even the keyboard disappears.
Jumping back doesn't work at all and the keyboard disappears always when the next active field is part of a different prototype cell (e.g. moving forward from the field for "brand" to the field for "quantity".
Has anyone a solution for it?
For the handling I have defined two notifications:
let keyBoardBarBackNotification = Notification.Name("keyBoardBarBackNotification")
let keyBoardBarNextNotification = Notification.Name("keyBoardBarNextNotification")
And the definition of the toolbar is done in the extension of 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)
}
}
In the viewController I have setup routines for the keyboard handling and a routine "switchActiveField" to determine the next actual field that should become the 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)
}
}
}
And the extension for the tableView is:
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!
}
Thanks for your support.

There are various ways to approach this... In fact, it's easy to find open-source 3rd-party libraries with lots of features -- just search (Google or wherever) for
swift ios form builder.But, if you'd like to work on it on your own, the basic idea is:
add your text fields to an array
add a class-level var/property such as
var activeField: UITextField?for each field, on
textFieldDidBeginEditing:when the user taps the "Next" button:
If all your fields are "on-screen" it's pretty straight-forward.
If they won't fit vertically (particularly when the keyboard is showing), if they're all in a scroll view, again, pretty straight-forward.
It gets complicated when putting them in cells in a tableView, for several reasons:
To add repeating similar-but-varying "rows," we don't need to use a table view.
For example, if we have a
UIStackViewwith.axis = .vertical:We've now added 10 single-label "cells."
So, for your task, instead of using a table view with your
LabelTextFieldCell, we can write this function:and a similar (but slightly more complex):
then use it similarly to
cellForRowAt:If we add that stackView to a scrollView, we have a scrollable "Form."
Here's a complete example you can try out (no
@IBOutletor@IBActionconnections ... just set a blank view controller's class toFormVC):We'll put our "Row View" builder funcs in extensions, just to keep the code separated and a bit more readable:
When running, it looks like this:
If you add some more "rows" - or, easier, increase the stack view spacing, such as
stackView.spacing = 100- you'll see how it continues to work with the scrollView when the keyboard is showing.Of course, you mention in your comments: "...more entry fields (e.g. date with a Datepicker, etc.)", so you'd need to write new "row builder" funcs and add some logic to Next tap going to/from a Picker instead of a textField.
But, you may find this a helpful starting point.