Created
February 16, 2022 17:01
-
-
Save shawn-frank/09339aea4ec8f5321c8877e97b585c22 to your computer and use it in GitHub Desktop.
A UITableView with editable UITextFields in UITableView cells that change based on the value of another
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// InputViewController.swift | |
// TestApp | |
// | |
// Created by Shawn Frank on 30/01/2022. | |
// | |
import UIKit | |
// Your model to store data in text fields | |
// Maybe for you, you will store currencies | |
// and base currency to multiply against | |
// I am just storing a random number for example | |
struct FinanceModel | |
{ | |
var number: Int? | |
} | |
// Custom UITextField which will store info | |
// about cell index path as we need to identify | |
// it when editing | |
class MappedTextField: UITextField | |
{ | |
var indexPath: IndexPath! | |
} | |
fileprivate class CustomCell: UITableViewCell | |
{ | |
// Use custom text field | |
var textField: MappedTextField! | |
static let tableViewCellIdentifier = "cell" | |
override init(style: UITableViewCell.CellStyle, | |
reuseIdentifier: String?) | |
{ | |
super.init(style: style, | |
reuseIdentifier: reuseIdentifier) | |
configureTextField() | |
} | |
required init?(coder: NSCoder) | |
{ | |
fatalError("init(coder:) has not been implemented") | |
} | |
// Configure text field and auto layout | |
private func configureTextField() | |
{ | |
textField = MappedTextField() | |
textField.keyboardType = .numberPad | |
textField.translatesAutoresizingMaskIntoConstraints = false | |
contentView.addSubview(textField) | |
textField.layer.borderWidth = 2.0 | |
textField.layer.borderColor = UIColor.blue.cgColor | |
textField.layer.cornerRadius = 5.0 | |
// auto-layout | |
textField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, | |
constant: 20).isActive = true | |
textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, | |
constant: -20).isActive = true | |
textField.topAnchor.constraint(equalTo: contentView.topAnchor, | |
constant: 20).isActive = true | |
textField.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, | |
constant: -20).isActive = true | |
} | |
} | |
class InputViewController: UIViewController | |
{ | |
let tableView = UITableView() | |
// Initialize an array of 12 finance models | |
var financeModels = | |
Array<FinanceModel>(repeating: FinanceModel(number: 0), count: 12) | |
// This will store current editing cell which is active | |
var activeTextFieldIndexPath: IndexPath? | |
// Array to store last interacted textfields | |
var inputQueue: [IndexPath] = [] | |
// Max capacity of input queue | |
let inputQueueCapacity = 2 | |
override func viewDidLoad() | |
{ | |
super.viewDidLoad() | |
// This is just view set up for me, | |
// You can ignore this | |
title = "TableView Input" | |
navigationController?.navigationBar.tintColor = .white | |
view.backgroundColor = .white | |
configureTableView() | |
} | |
// Configure TableView and layout | |
private func configureTableView() | |
{ | |
tableView.translatesAutoresizingMaskIntoConstraints = false | |
tableView.register(CustomCell.self, | |
forCellReuseIdentifier: CustomCell.tableViewCellIdentifier) | |
tableView.dataSource = self | |
tableView.delegate = self | |
// remove additional rows | |
tableView.tableFooterView = UIView() | |
view.addSubview(tableView) | |
// Auto layout | |
tableView.leadingAnchor | |
.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true | |
tableView.topAnchor | |
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true | |
tableView.trailingAnchor | |
.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true | |
tableView.bottomAnchor | |
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true | |
} | |
} | |
extension InputViewController: UITableViewDataSource | |
{ | |
func tableView(_ tableView: UITableView, | |
numberOfRowsInSection section: Int) -> Int | |
{ | |
// Random number for me | |
return financeModels.count | |
} | |
func tableView(_ tableView: UITableView, | |
cellForRowAt indexPath: IndexPath) -> UITableViewCell | |
{ | |
let cell = | |
tableView.dequeueReusableCell(withIdentifier: | |
CustomCell.tableViewCellIdentifier) | |
as! CustomCell | |
// Set index path of custom text field | |
cell.textField.indexPath = indexPath | |
// Set view controller to respond to text field events | |
cell.textField.delegate = self | |
cell.selectionStyle = .none | |
// Check if any cell is currently active | |
// && if we are current cell is NOT the active one | |
// We want to leave active cell untouched | |
if let activeTextFieldIndexPath = activeTextFieldIndexPath, | |
activeTextFieldIndexPath.row != indexPath.row | |
{ | |
updateCell(cell, atIndexPath: indexPath) | |
} | |
else | |
{ | |
// no active cell, just set the text | |
updateCell(cell, atIndexPath: indexPath) | |
} | |
return cell | |
} | |
private func updateCell(_ cell: CustomCell, | |
atIndexPath indexPath: IndexPath) | |
{ | |
// Retrieve number currently stored in model | |
if let number = financeModels[indexPath.row].number | |
{ | |
// Set text of number in model * row number | |
// Do any calculation you like, this is just an | |
// example | |
cell.textField.text = "\(number)" | |
} | |
else | |
{ | |
// If no valid number, set blank | |
cell.textField.text = "" | |
} | |
} | |
} | |
extension InputViewController: UITextFieldDelegate | |
{ | |
// Respond to new text in the text field | |
func textFieldDidChangeSelection(_ textField: UITextField) | |
{ | |
// Get the coordinates of where we tapped in the table | |
let tapLocation = textField.convert(textField.bounds.origin, | |
to: tableView) | |
// 1. Convert generic UITextField to MappedTextField | |
// 2. && Retrieve index path from where we tapped | |
// 3. && Retrieve text from the text field | |
// 4. && Check if text is valid number | |
// 5. && Only make changes when text field changes | |
if let textField = textField as? MappedTextField, | |
let indexPath = self.tableView.indexPathForRow(at: tapLocation), | |
let text = textField.text, | |
let number = Int(text), | |
number != financeModels[indexPath.row].number | |
{ | |
// Assign local variable with index path we are editing | |
activeTextFieldIndexPath = indexPath | |
// dummy calculations | |
if number > 0 | |
{ | |
// Update all calculations in finance model | |
for (index, _) in financeModels.enumerated() | |
{ | |
// update current value in text field | |
if index == indexPath.row | |
{ | |
financeModels[index].number = number | |
continue | |
} | |
// Random calculations for other cells | |
// Use your own calculation | |
financeModels[index].number = number * index | |
} | |
} | |
// We only want to update data in visible rows so | |
// get all the index paths of visible rows | |
let visibleRows = self.tableView.indexPathsForVisibleRows ?? [] | |
// We want to update all rows EXCEPT active row | |
// so do a filter for this | |
let allRowsWithoutActive = (visibleRows).filter | |
{ | |
// Remove the active index path from the | |
// visible index paths | |
$0.section != indexPath.section || | |
$0.row != indexPath.row | |
} | |
// Reload the visible rows EXCEPT active | |
self.tableView.reloadRows(at: allRowsWithoutActive, | |
with: .automatic) { | |
print("done") | |
} | |
} | |
} | |
func textFieldDidBeginEditing(_ textField: UITextField) | |
{ | |
// Get the coordinates of where we tapped in the table | |
let tapLocation = textField.convert(textField.bounds.origin, | |
to: tableView) | |
// 1. Get the of the tapped text field | |
// 2. Make sure this index path is not being tracked | |
if let indexPath = self.tableView.indexPathForRow(at: tapLocation), | |
!inputQueue.contains(indexPath) | |
{ | |
// You can make it blank | |
textField.text = "" | |
} | |
// The text field is being tracked so do nothing | |
} | |
func textFieldDidEndEditing(_ textField: UITextField) | |
{ | |
// Get the coordinates of where we tapped in the table | |
let tapLocation = textField.convert(textField.bounds.origin, | |
to: tableView) | |
// 1. Convert UITextField to MappedTextField (my custom class you can ignore) | |
// 2. && Retrieve index path from where we tapped | |
// 3. && Retrieve text from the text field | |
// 4. && Check if text is valid number | |
if let textField = textField as? MappedTextField, | |
let indexPath = self.tableView.indexPathForRow(at: tapLocation), | |
let text = textField.text, | |
let number = Int(text) | |
{ | |
// Check input queue does not contain index path | |
// && that user has entered some valid number | |
if !inputQueue.contains(indexPath) | |
&& number > 0 | |
{ | |
// Check if you reached max capacity | |
if inputQueue.count >= inputQueueCapacity | |
{ | |
// Dequeue from the front of the queue | |
inputQueue.removeFirst() | |
} | |
// Append index path to queue as it is not in queue | |
inputQueue.append(indexPath) | |
} | |
} | |
// Reset currently active text field | |
activeTextFieldIndexPath = nil | |
} | |
} | |
extension InputViewController: UITableViewDelegate | |
{ | |
func scrollViewDidScroll(_ scrollView: UIScrollView) | |
{ | |
// End editing when scrolling table view | |
// This is for me, you can have another way | |
view.endEditing(true) | |
} | |
func tableView(_ tableView: UITableView, | |
heightForRowAt indexPath: IndexPath) -> CGFloat | |
{ | |
// Return a random height | |
return 80 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment