self sizing cell with uitableviewdiffabledatasource - ios

I have a detail ViewController whoose cells are defined by a custom uitableviewdiffabledata like this:
{ (_, indexPath, item) -> UITableViewCell? in
let color = UIColor(named: "blue")!
if let _ = item as? TextFieldItem, let cell = tableView.dequeueReusableCell(withIdentifier: "textField", for: indexPath) as? TextFieldTableViewCell {
cell.textField.text = recipe.wrappedValue.name
cell.textField.placeholder = NSLocalizedString("name", comment: "")
cell.selectionStyle = .none
cell.textChanged = nameChanged
cell.backgroundColor = color
return cell
} else if let imageItem = item as? ImageItem, let imageCell = tableView.dequeueReusableCell(withIdentifier: "image", for: indexPath) as? ImageTableViewCell {
imageCell.setup(imageData: imageItem.imageData)
return imageCell
} else if let _ = item as? AmountItem, let amountCell = tableView.dequeueReusableCell(withIdentifier: "times", for: indexPath) as? AmountTableViewCell{
amountCell.setUp(with: recipe.wrappedValue.timesText, format: formatAmount)
amountCell.backgroundColor = color
return amountCell
} else if item is InfoItem {
return InfoTableViewCell(infoText: Binding(get: {
return recipe.wrappedValue.info
}, set: updateInfo), reuseIdentifier: "info")
} else if let stripItem = item as? InfoStripItem, let infoStripCell = tableView.dequeueReusableCell(withIdentifier: "infoStrip", for: indexPath) as? InfoStripTableViewCell {
infoStripCell.setUpCell(for: stripItem)
return infoStripCell
} else if let stepItem = item as? StepItem {
let stepCell = StepTableViewCell(style: .default, reuseIdentifier: "step")
stepCell.setUpCell(for: stepItem.step)
return stepCell
} else if let detailItem = item as? DetailItem, let cell = tableView.dequeueReusableCell(withIdentifier: "detail", for: indexPath) as? DetailTableViewCell {
let title = NSAttributedString(string: detailItem.text, attributes: [.foregroundColor : UIColor.label])
cell.textLabel?.attributedText = title
cell.accessoryType = .disclosureIndicator
cell.backgroundColor = color
return cell
}
return UITableViewCell()
}
and i want to make the InfoTableViewCell which is defined like this:
class InfoTableViewCell: UITableViewCell {
#Binding private var infoText: String
private var textView = UITextView()
init(infoText: Binding<String>, reuseIdentifier: String?) {
self._infoText = infoText
super.init(style: .default, reuseIdentifier: reuseIdentifier)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.addSubview(textView)
textView.fillSuperview()
textView.text = infoText
textView.delegate = self
textView.backgroundColor = UIColor(named: "blue")!
textView.font = UIFont.preferredFont(forTextStyle: .body)
}
}
extension InfoTableViewCell: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
self.infoText = textView.text
}
}
and i want to make this cell to resize based on the text thats in the textField. Any tips on how to do that ?
P. S. I am using the LBTA tools for filling the contents of the cell with the textField.

In my experience, just omit the tableView(_:heightForRowAt:) tableview delegate. Then when creating your cells, create constraints that bind the top and bottom of your cell to the top and bottom of your content. The cell will then grow or shrink to meet the constraints.
In the init of your custom table view cell class:
textView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8).isActive = true
textView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8).isActive = true
Not sure how the LBTA tools will affect the behavior of this, so this might only work with vanilla UIKit.

Related

Adding top and bottom constraints causes UILable to be squished

Programmatically I created a custom UITableViewCell and tried centering two UILabels vertically inside it. But the UILabel ended up being squished. Doing the same thing in Interface Builder with a prototype cell works well. What is wrong with my code?
The Custom view cell class
import UIKit
class TopViewCell: UITableViewCell {
let df: DateFormatter = {
let df = DateFormatter()
df.dateFormat = NSLocalizedString("DATE_WEEKDAY", comment: "show date and weekday")
return df
}()
var dateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var costLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let margin = contentView.layoutMarginsGuide
contentView.addSubview(dateLabel)
dateLabel.leadingAnchor.constraint(equalTo: margin.leadingAnchor).isActive = true
dateLabel.topAnchor.constraint(equalTo: margin.topAnchor).isActive = true
dateLabel.bottomAnchor.constraint(equalTo: margin.bottomAnchor).isActive = true
contentView.addSubview(costLabel)
costLabel.trailingAnchor.constraint(equalTo: margin.trailingAnchor).isActive = true
costLabel.topAnchor.constraint(equalTo: dateLabel.topAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
dateLabel.text = df.string(from: Date())
costLabel.text = "total: five thousand"
}
}
The Custom UITableViewController class
import UIKit
class ItemViewController: UITableViewController {
var itemStore: ItemStore!
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(TopViewCell.self, forCellReuseIdentifier: "top_cell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemStore.allItems.count + 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: UITableViewCell!
if indexPath.row == 0 {
cell = tableView.dequeueReusableCell(withIdentifier: "top_cell", for: indexPath)
} else {
cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = itemStore.allItems[indexPath.row - 1].name
cell.textLabel?.font = cell.textLabel!.font.withSize(30)
cell.detailTextLabel?.text = "$\(itemStore.allItems[indexPath.row - 1].valueInDolloar)"
}
return cell
}
}
Your TopViewCell is not auto-sizing correctly because you're setting the text in layoutSubviews(). Move those two lines to init and it will size properly:
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let margin = contentView.layoutMarginsGuide
contentView.addSubview(dateLabel)
dateLabel.leadingAnchor.constraint(equalTo: margin.leadingAnchor).isActive = true
dateLabel.topAnchor.constraint(equalTo: margin.topAnchor).isActive = true
dateLabel.bottomAnchor.constraint(equalTo: margin.bottomAnchor).isActive = true
contentView.addSubview(costLabel)
costLabel.trailingAnchor.constraint(equalTo: margin.trailingAnchor).isActive = true
costLabel.topAnchor.constraint(equalTo: dateLabel.topAnchor).isActive = true
// set the text here
dateLabel.text = df.string(from: Date())
costLabel.text = "total: five thousand"
}
As a side note, you should specify the class when you use TopViewCell:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "top_cell", for: indexPath) as! TopViewCell
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = itemStore.allItems[indexPath.row - 1].name
cell.textLabel?.font = cell.textLabel!.font.withSize(30)
cell.detailTextLabel?.text = "$\(itemStore.allItems[indexPath.row - 1].valueInDolloar)"
return cell
}
As another side note... you can create two prototype cells in your Storyboard.

Can not validate if textfield is empty

I am posting this after having tried all the .isEmpty solutions i found.
I am unable to detect a value in textField. I have cells which are set to cell.selectionStyle = .none. These cells have a label and textFields.
I have given the cells identifiers:
let cell = addRestaurant.dequeueReusableCell(withIdentifier: String(describing: RestaurantAddViewCells.self), for: indexPath) as! RestaurantAddViewCells
My goal is to have a button which checks if any field is empty onclick.
let saveButton = UIBarButtonItem(image: UIImage(named: "save"), style: .plain, target: self, action: #selector(saveRestaurant))
Here is how i try to check if the textFields are empty or not:
#objc func saveRestaurant() {
if let indexPath = addRestaurant.indexPathForSelectedRow {
let cell = addRestaurant.cellForRow(at: indexPath) as! RestaurantAddViewCells
if (cell.nameTextField.text == "" || cell.typeTextField.text == "" || cell.locationTextField.text == "" || cell.hotelPhoneText.text == "") {
let saveAlertController = UIAlertController(title: "Fields Empty", message: "fill all fields", preferredStyle: .alert)
let saveAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
saveAlertController.addAction(saveAction)
self.present(saveAlertController, animated: false, completion: nil)
}
else { }
}
}
However nothing happens
I have tried and comment cell.selectionStyle = .none, still no effect
Here addRestaurant is tableView in same file while RestaurantAddViewCells is a class containing properties for labels , textFields
UPDATE - here is what i do in my cellForRowAt, sample for first two cells
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
case 0:
let cell = addRestaurant.dequeueReusableCell(withIdentifier: String(describing: RestaurantAddViewCells.self), for: indexPath) as! RestaurantAddViewCells
view.addSubview(cell.contentView)
view.addSubview(cell.hotelImage)
view.addSubview(cell.imageButton)
view.addSubview(cell)
cell.imageButton.translatesAutoresizingMaskIntoConstraints = false
cell.hotelImage.translatesAutoresizingMaskIntoConstraints = false
//set the cell height
cell.heightAnchor.constraint(greaterThanOrEqualToConstant: 200).isActive = true
//set the hotelImage
cell.hotelImage.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
cell.hotelImage.heightAnchor.constraint(equalToConstant: 200).isActive = true
cell.hotelImage.leadingAnchor.constraint(equalTo: cell.leadingAnchor).isActive = true
cell.hotelImage.topAnchor.constraint(equalTo: cell.topAnchor).isActive = true
// pin it
//No need to pin it as width is already pinned to to lead and trail of screen
cell.imageButton.heightAnchor.constraint(equalToConstant: 30).isActive = true
cell.imageButton.widthAnchor.constraint(equalToConstant: 30).isActive = true
cell.imageButton.centerXAnchor.constraint(equalTo: cell.centerXAnchor).isActive = true
cell.imageButton.centerYAnchor.constraint(equalTo: cell.centerYAnchor).isActive = true
cell.imageButton.image = UIImage(named: "photo")
return cell
case 1:
let cell = addRestaurant.dequeueReusableCell(withIdentifier: String(describing: RestaurantAddViewCells.self), for: indexPath) as! RestaurantAddViewCells
cell.nameTextFiled.tag = 1
cell.nameTextFiled.delegate = self
cell.nameTextFiled.becomeFirstResponder()
cell.selectionStyle = .none
cell.heightAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true
view.addSubview(cell.nameLabel)
view.addSubview(cell.nameTextFiled)
view.addSubview(cell)
view.addSubview(cell.contentView)
cell.nameLabel.translatesAutoresizingMaskIntoConstraints = false
cell.nameTextFiled.translatesAutoresizingMaskIntoConstraints = false
cell.nameTextFiled.heightAnchor.constraint(equalToConstant: 50).isActive = true
cell.nameLabel.text = "Name:"
//Define custom fonts
let font = UIFont(name: "Rubik-Medium", size: 18)
let dynamicFonts = UIFontMetrics(forTextStyle: .body)
cell.nameLabel.font = dynamicFonts.scaledFont(for: font!)
cell.nameTextFiled.font = dynamicFonts.scaledFont(for: font!)
cell.nameTextFiled.borderStyle = .roundedRect
cell.nameTextFiled.placeholder = "Enter Your Name"
let stackName = UIStackView()
view.addSubview(stackName)
stackName.alignment = .top
stackName.axis = .vertical
stackName.spacing = 5.0
stackName.distribution = .fill
stackName.translatesAutoresizingMaskIntoConstraints = false
stackName.addArrangedSubview(cell.nameLabel)
stackName.addArrangedSubview(cell.nameTextFiled)
stackName.topAnchor.constraint(equalTo: cell.topAnchor, constant: 10).isActive = true
stackName.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 10).isActive = true
stackName.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -10).isActive = true
cell.nameTextFiled.trailingAnchor.constraint(equalTo: stackName.trailingAnchor).isActive = true
return cell
This is what my updated RestaurantAddViewCells lookslike with input from one member here
class RestaurantAddViewCells: UITableViewCell, UITextFieldDelegate, UITextViewDelegate {
var nameLabel: UILabel = UILabel()
var nameTextFiled: RoundedTextFields = RoundedTextFields()
var typeLabel: UILabel = UILabel()
var typeTextField: RoundedTextFields = RoundedTextFields()
var locationLabel: UILabel = UILabel()
var locationTextField: RoundedTextFields = RoundedTextFields()
var imageButton: UIImageView = UIImageView()
var hotelImage: UIImageView = UIImageView()
var hotelDescriptionLabel: UILabel = UILabel()
var hotelTextDescription: UITextView = UITextView()
var hotelPhonelabel: UILabel = UILabel()
var hotelPhoneText: RoundedTextFields = RoundedTextFields()
var isEmptyTextFields: Bool {
return nameTextFiled.text!.isEmpty || typeTextField.text!.isEmpty || locationTextField.text!.isEmpty || hotelTextDescription.text!.isEmpty || hotelPhoneText.text!.isEmpty
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
UPDATE 2 - so here is my update while trying to ask the question again, i have a tableview where each cell has some textfields, when i press a button before i do any thing with the data, i want to check if the fields are filled or not, how can i do this, now as per my design i use a class to define the view properties and call them and give then values where i use the tableView, i use cellForRowAt to define constraints and provide values to labels etc, but how can i successfully access them outside of cellForRowAt to check current state , with out totally changing the design of project
UPDATE 3 - it seems if i were to do the same thing in story board and add the outlets to same function it is capable of detecting a change in state of textbox, if it is empty or not, in case of trying to do it programatically, its not able to detect a change in state of textbox from empty to not empty, but i still have no way to check like in javascript if the textboxes are empty or not on button click
Update 4 - I am now using below code on button tap, but for some strange reason , i am not able to detect the text entered, it always keeps returning empty even if there is text there
#objc func saveRestaurant(sender: AnyObject) {
let cell = addRestaurant.dequeueReusableCell(withIdentifier: String(describing: RestaurantAddViewCells.self)) as! RestaurantAddViewCells
if cell.nameTextFiled.text == "" || cell.typeTextField.text == "" || cell.locationTextField.text == "" || cell.hotelPhoneText.text == "" || cell.hotelTextDescription.text == "" {
let alertController = UIAlertController(title: "Oops", message: "We can't proceed because one of the fields is blank. Please note that all fields are required.", preferredStyle: .alert)
let alertAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(alertAction)
present(alertController, animated: true, completion: nil)
// addRestaurant.reloadData()
return
}
else {
print("Name: \(cell.nameTextFiled.text ?? "")")
print("Type: \(cell.typeTextField.text ?? "")")
print("Location: \(cell.locationTextField.text ?? "")")
print("Phone: \(cell.hotelPhoneText.text ?? "")")
print("Description: \(cell.hotelTextDescription.text ?? "")")
dismiss(animated: true, completion: nil)
}
}
Update 5 - this is what has finally worked , but is very long and not so good, can any one suggest a loop for going through all the indexPath
#objc func saveRestaurant(sender: AnyObject) {
let index = IndexPath(row: 1, section: 0)
let cell: RestaurantAddViewCells = self.addRestaurant.cellForRow(at: index) as! RestaurantAddViewCells
let nameVal = cell.nameTextFiled.text!
let index1 = IndexPath(row: 2, section: 0)
let cell2: RestaurantAddViewCells = self.addRestaurant.cellForRow(at: index1) as! RestaurantAddViewCells
let typeVal = cell2.typeTextField.text!
let index2 = IndexPath(row: 3, section: 0)
let cell3: RestaurantAddViewCells = self.addRestaurant.cellForRow(at: index2) as! RestaurantAddViewCells
let locationVal = cell3.locationTextField.text!
let index3 = IndexPath(row: 4, section: 0)
let cell4: RestaurantAddViewCells = self.addRestaurant.cellForRow(at: index3) as! RestaurantAddViewCells
let phoneVal = cell4.hotelPhoneText.text!
let index4 = IndexPath(row: 5, section: 0)
let cell5: RestaurantAddViewCells = self.addRestaurant.cellForRow(at: index4) as! RestaurantAddViewCells
let descVal = cell5.hotelTextDescription.text!
if(nameVal == "" || typeVal == "" || locationVal == "" || phoneVal == "" || descVal == "") {
let saveAlertController = UIAlertController(title: "Fields Empty", message: "fill all fields", preferredStyle: .alert)
let saveAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
saveAlertController.addAction(saveAction)
self.present(saveAlertController, animated: false, completion: nil)
// return
}
else {
print("Name: \(nameVal)")
print("Type: \(typeVal)")
print("Location: \(locationVal)")
print("Phone: \(phoneVal)")
print("Description: \(descVal)")
self.navigationController?.popViewController(animated: false)
}
}
First of all, add a computed property in RestaurantAddViewCells that'll return if any of the textFields in the cell is empty, i.e.
class RestaurantAddViewCells: UITableViewCell {
#IBOutlet weak var nameTextField: UITextField!
#IBOutlet weak var typeTextField: UITextField!
var isEmptyTextFields: Bool {
return nameTextField.text!.isEmpty || typeTextField.text!.isEmpty
}
}
Now, your saveRestaurant() method will loop through the numberOfCells. Get the cell for each row and check if isEmptyTextFields returns true. In that case you need to show the alert.
#objc func saveRestaurant() {
let numberOfCells = 6
var isAnyTextFieldEmpty = false
(0..<numberOfCells).forEach {
let cell = tableView.cellForRow(at: IndexPath(row: $0, section: 0)) as! RestaurantAddViewCells
isAnyTextFieldEmpty = isAnyTextFieldEmpty || cell.isEmptyTextFields
}
if isAnyTextFieldEmpty {
//Show alert...
} else {
//....
}
}
I'll try to give you an answer as easy as I can make possible.
Problem
You're using a table view and it's cell which contains a textfield where user may enter some input, also the cell may get reused when scrolled and its possible we may lose input from user.
Solution
We have a textfield in our cells, and we need all the inputs from user to be stored somewhere lets say a dictionary of IndexPath as key and String as value. Something like var inputs:[IndexPath: String] = [:]. Whenever user enters something and as soon as leaves the textfield we'll store that input in our dictionary against it's cell indexPath. when user clicks on button, we'll loop through and check which textfield is empty
A very simple example is here
import UIKit
class TextFieldTableViewCell: UITableViewCell {
lazy var textField: UITextField = {
let textField = UITextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.placeholder = "Enter your text here"
return textField
} ()
lazy var label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.setContentHuggingPriority(.required, for: .horizontal)
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension TextFieldTableViewCell {
//MARK: Private
private func setupView() {
selectionStyle = .none
contentView.addSubview(label)
contentView.addSubview(textField)
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8).isActive = true
label.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8).isActive = true
label.bottomAnchor.constraint(greaterThanOrEqualTo: contentView.bottomAnchor, constant: -8).isActive = true
label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
textField.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8).isActive = true
textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8).isActive = true
textField.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8).isActive = true
textField.bottomAnchor.constraint(greaterThanOrEqualTo: contentView.bottomAnchor, constant: -8).isActive = true
textField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
}
}
class ViewController: UIViewController {
private lazy var tableView: UITableView = {
let tableView = UITableView.init(frame: .zero, style: .grouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
return tableView
}()
private var inputs: [IndexPath: String] = [:]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.dataSource = self
tableView.register(TextFieldTableViewCell.self, forCellReuseIdentifier: "TextFieldTableViewCell")
title = "Some random title"
let barbutton = UIBarButtonItem.init(barButtonSystemItem: .done, target: self, action: #selector(saveAction(_:)))
navigationItem.rightBarButtonItem = barbutton
}
#objc
func saveAction(_ sender: UIBarButtonItem) {
view.endEditing(true)
for i in 0 ..< tableView.numberOfSections {
for j in 0 ..< tableView.numberOfRows(inSection: i) {
let indexPath = IndexPath.init(row: j, section: i)
print("Input at indexPath: Row: \(indexPath.row), Section: \(indexPath.section)")
if let input = inputs[indexPath], input.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
print(input)
}
else {
print("user has not input any value or kept it empty")
}
print("__________")
}
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TextFieldTableViewCell", for: indexPath) as? TextFieldTableViewCell ?? TextFieldTableViewCell.init(style: .default, reuseIdentifier: "TextFieldTableViewCell")
cell.label.text = "Row: \(indexPath.row)"
cell.textField.delegate = self
cell.textField.text = inputs[indexPath]
return cell
}
}
extension ViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
if let cell = textField.superview?.superview as? TextFieldTableViewCell, let indexPath = tableView.indexPath(for: cell) {
inputs[indexPath] = textField.text
}
}
}
EDIT
After OPs comments, here's the solution that will work for OP
Solution:
As OP has only 6 cells, we can cache then in a dictionary and return from the dictionary whenever needed. (We're doing this only because of small number of table cells and OP's structure. I do not recommend this solution)
Add a new dictionary to your viewcontroller
private var cachedCell: [IndexPath: TextFieldTableViewCell] = [:]
Change Cell for row to this (Note that you should not reuse tableview cell as we're caching them in our cachedCell dictionary otherwise it may result in unknown. It is never recommended to cache cells by me as well as other developers)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = cachedCell[indexPath] {
return cell
}
let cell = TextFieldTableViewCell.init(style: .default, reuseIdentifier: "TextFieldTableViewCell")
cell.label.text = "Row: \(indexPath.row)"
cell.textField.delegate = self
cell.textField.text = inputs[indexPath]
cachedCell[indexPath] = cell
return cell
}
Change Save Action to
#objc
func saveAction(_ sender: UIBarButtonItem) {
view.endEditing(true)
for (indexPath, cell) in cachedCell {
print("Input at indexPath: Row: \(indexPath.row), Section: \(indexPath.section)")
if let input = cell.textField.text, input.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
print(input)
}
else {
print("user has not input any value or kept it empty")
}
print("__________")
}
}
If you have any other questions, you can ask it in comment section.
Happy Coding

Dynamically resize TableViewController Cell

In my project I have a SignUpViewController which looks like this:
All the textFields are custom-cells within a tableViewController.
TableView:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 7
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 1st cell -> email textfield
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpEmailCell", for: indexPath) as! SignUpEmailCell
return cell
// 2nd cell -> anzeigename
}else if indexPath.row == 1 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpAnzeigeName", for: indexPath) as! SignUpAnzeigeName
return cell
// 3rd cell -> Wishlist-Handle
}else if indexPath.row == 2 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpHandleCell", for: indexPath) as! SignUpHandleCell
return cell
// 4th cell -> passwort textfield
}else if indexPath.row == 3 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpPasswordCell", for: indexPath) as! SignUpPasswordCell
return cell
// 5th cell -> repeat password textfield
}else if indexPath.row == 4 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpPasswordRepeatCell", for: indexPath) as! SignUpPasswordRepeatCell
return cell
// 6th cell -> document label
}else if indexPath.row == 5 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpDocumentCell", for: indexPath) as! SignUpDocumentCell
return cell
}
// last cell -> signUpButton
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpButtonCell", for: indexPath) as! SignUpButtonCell
return cell
}
Password-Cell: (basic structure is the same for every cell)
class SignUpPasswordCell: UITableViewCell, UITextFieldDelegate {
public static let reuseID = "SignUpPasswordCell"
lazy var eyeButton: UIButton = {
let v = UIButton()
v.addTarget(self, action: #selector(eyeButtonTapped), for: .touchUpInside)
v.setImage(UIImage(named: "eyeOpen"), for: .normal)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
lazy var passwordTextField: CustomTextField = {
let v = CustomTextField()
v.borderActiveColor = .white
v.borderInactiveColor = .white
v.textColor = .white
v.font = UIFont(name: "AvenirNext-Regular", size: 17)
v.placeholder = "Passwort"
v.placeholderColor = .white
v.placeholderFontScale = 0.8
v.minimumFontSize = 13
v.borderStyle = .line
v.addTarget(self, action: #selector(SignUpPasswordCell.passwordTextFieldDidChange(_:)),for: .editingChanged)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = .clear
passwordTextField.delegate = self
eyeButton.isHidden = true
passwordTextField.textContentType = .newPassword
passwordTextField.isSecureTextEntry.toggle()
setupViews()
}
func setupViews(){
contentView.addSubview(passwordTextField)
contentView.addSubview(eyeButton)
passwordTextField.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
passwordTextField.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
passwordTextField.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
passwordTextField.heightAnchor.constraint(equalToConstant: 60).isActive = true
eyeButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5).isActive = true
eyeButton.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 10).isActive = true
}
var check = true
#objc func eyeButtonTapped(_ sender: Any) {
check = !check
if check == true {
eyeButton.setImage(UIImage(named: "eyeOpen"), for: .normal)
} else {
eyeButton.setImage(UIImage(named: "eyeClosed"), for: .normal)
}
passwordTextField.isSecureTextEntry.toggle()
if let existingText = passwordTextField.text, passwordTextField.isSecureTextEntry {
/* When toggling to secure text, all text will be purged if the user
continues typing unless we intervene. This is prevented by first
deleting the existing text and then recovering the original text. */
passwordTextField.deleteBackward()
if let textRange = passwordTextField.textRange(from: passwordTextField.beginningOfDocument, to: passwordTextField.endOfDocument) {
passwordTextField.replace(textRange, withText: existingText)
}
}
/* Reset the selected text range since the cursor can end up in the wrong
position after a toggle because the text might vary in width */
if let existingSelectedTextRange = passwordTextField.selectedTextRange {
passwordTextField.selectedTextRange = nil
passwordTextField.selectedTextRange = existingSelectedTextRange
}
}
#objc func passwordTextFieldDidChange(_ textField: UITextField) {
if textField.text == "" {
self.eyeButton.isHidden = true
}else {
self.eyeButton.isHidden = false
}
}
}
Problem:
I would like to be able to show some extra information on some textFields when selected.
For example: When passwordTextField is editing I would like to show the password requirements right below the textfield. But the extra information should only be displayed while editing or after editing. When the ViewController is being displayed at first it should still look like the picture above.
I hope my problem is clear and I am grateful for every help.

Swift 4: Button triggers the same action in another UITableViewCell

I have a cell with 2 buttons (for listening and speech recognition), label, and textfield. What I am trying to achieve is when the speech recognition button is selected the user speaks the content that is displayed in a label.
My issue with this is that listening button works fine according to the indexPath.row, but the speaking button doesn't. As when it is active, the button in another cell is becoming active too. And it records the same in those cells.
You can see the picture of what I am talking about here
The methods for listening (which is audio synthesizer) and speech recognition are in the UITableViewCell. I have tried all the solutions I could find online, none of them did the trick. Have tried
protocol RepeatCellDelegate: class {
func buttonTapped(cell: RepeatCell)
}
but the problem remains the same. Also, have created another project and instead of using the button to do speech recognition I just used direct textField input, still the same problem occurs.
Button in TableViewCell class:
#IBAction func speakButtonPressed(_ sender: Any) {
self.delegate?.buttonTapped(cell: self)
}
My cellForRowAt indexPath:
let cell = tableView.dequeueReusableCell(withIdentifier: "RepeatCell") as! RepeatCell
cell.delegate = self
cell.conditionlabel.text = repeatTask[indexPath.row].conditionLabel
return cell
The buttonTapped function which detects the cell index and record speech input. It prints right cell index after the button is tapped, but the action gets triggered in another cell too.
func buttonTapped(cell: RepeatCell) {
guard let indexPath = self.repeatTV.indexPath(for: cell) else {
return
}
cell.speakButton.isSelected = !cell.speakButton.isSelected
if (cell.speakButton.isSelected){
self.recordAndRecognizeSpeech()
} else {
audioEngine.inputNode.removeTap(onBus: 0)
recognitionTask?.cancel()
}
print("Button tapped on row \(indexPath.row)")
}
// the speech input recognizer function:
// variables for speech recognizer
let audioEngine = AVAudioEngine()
let speechRecognizer: SFSpeechRecognizer? = SFSpeechRecognizer(locale: Locale.init(identifier: "en-US"))
let request = SFSpeechAudioBufferRecognitionRequest()
var recognitionTask: SFSpeechRecognitionTask?
// speech function
func recordAndRecognizeSpeech(){
let node = audioEngine.inputNode
let recordingFormat = node.outputFormat(forBus: 0)
node.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, _ in
self.request.append(buffer)
}
audioEngine.prepare()
do {
try audioEngine.start()
} catch {
return print(error)
}
guard let myRecognizer = SFSpeechRecognizer() else {
return
}
if !myRecognizer.isAvailable {
return
}
recognitionTask = speechRecognizer?.recognitionTask(with: request, resultHandler: { (result, error) in
if result != nil { //
if let result = result{
let cell = self.repeatTV.dequeueReusableCell(withIdentifier: "RepeatCell") as! RepeatCell
let bestString = result.bestTranscription.formattedString
if cell.speakButton.isSelected == true {
cell.userInput.text = bestString
}
}else if let error = error{
print(error)
}
}
})
}
I get data from a local JSON file and this is a model:
struct RepeatTask: Codable {
let name: String
let label: String
let conditionWord: String
}
Perhaps someone could help me with this?
There isn't enough code here to re-create your issue, in the future please provide a Minimal, Complete, and Verifiable example. Unfortunately, no one can offer you a effective solution to help you resolve the issue if they cannot re-create the problem.
However I believe I understand what you are trying to accomplish:
A Model Object i.e. a struct.
A Protocol with a default implementation that is the same for all cells.
A TableViewCell class conforming to the protocol that calls the protocol's methods.
A TableViewDelegate and Datasource to manage the objects from 1.
Consider the following:
import UIKit
/// 1.
/// Data model for "Repeat Cell Objects"
struct RepeaterModel {
var outputText:String?
var inputAudio:Data?
}
/// 2.
/// Allows a cell to delegate listening and repeating (speaking)
protocol RepeatableCell {
func listen()
func speak()
}
// Extend your protocol to add a default implementation,
// that way you can just confrom to the protocol
// without implementing it every time, in every cell class.
extension RepeatableCell {
func listen() {
print("default implementation for listen")
}
func speak(){
print("default implementation for speak")
}
}
/// 3.
final class RepeatCell: UITableViewCell, RepeatableCell {
// MARK: - Properties
var model:RepeaterModel? {
didSet {
DispatchQueue.main.async {
self.titleLabel.text = self.model?.outputText
}
}
}
// MARK: - Views
lazy var listenButton: UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("Listen", for: .normal)
btn.addTarget(self, action: #selector(activateListen), for: .touchUpInside)
btn.setTitleColor(.white, for: .normal)
btn.backgroundColor = .blue
btn.translatesAutoresizingMaskIntoConstraints = false
return btn
}()
lazy var speakButton: UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("Speak", for: .normal)
btn.addTarget(self, action: #selector(activateSpeak), for: .touchUpInside)
btn.setTitleColor(.white, for: .normal)
btn.backgroundColor = .green
btn.translatesAutoresizingMaskIntoConstraints = false
return btn
}()
let titleLabel: UILabel = {
let l = UILabel()
l.translatesAutoresizingMaskIntoConstraints = false
l.textColor = .black
l.textAlignment = .center
l.text = "No Text"
return l
}()
//MARK: - Initializers
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Class Methods
func setup() {
self.contentView.addSubview(listenButton)
self.contentView.addSubview(speakButton)
self.contentView.addSubview(titleLabel)
let spacing: CGFloat = 25.0
//Listen top left
listenButton.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: spacing).isActive = true
listenButton.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: spacing).isActive = true
listenButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
listenButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
// title label, center top.
titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: spacing).isActive = true
titleLabel.leadingAnchor.constraint(equalTo: self.listenButton.trailingAnchor, constant: spacing).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: self.speakButton.leadingAnchor, constant: -spacing).isActive = true
titleLabel.heightAnchor.constraint(equalToConstant: 50).isActive = true
//Speak top right
speakButton.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: spacing).isActive = true
speakButton.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -spacing).isActive = true
speakButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
speakButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
#objc func activateListen() {
print("listen was pressed! on cell \(self.model?.outputText ?? "No Text")")
/// The user wants to listen
// call the delegate method..
listen()
// use self.model?.outputText
}
#objc func activateSpeak() {
print("Speak was pressed! on cell \(self.model?.outputText ?? "No Text")")
/// The user is speaking, record audio
// call the delegate method..
speak()
//self.model?.inputAudio = somedata
}
}
/// 4.
class ViewController: UITableViewController {
// Array of your model objects
var objects:[RepeaterModel] = []
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(RepeatCell.self, forCellReuseIdentifier: "Repeat")
// create or fetch model objects
let items = [
RepeaterModel(outputText: "1st Cell", inputAudio: nil),
RepeaterModel(outputText: "2nd Cell", inputAudio: nil),
RepeaterModel(outputText: "3rd Cell", inputAudio: nil),
RepeaterModel(outputText: "4th Cell", inputAudio: nil),
RepeaterModel(outputText: "5th Cell", inputAudio: nil),
RepeaterModel(outputText: "6th Cell", inputAudio: nil),
RepeaterModel(outputText: "8th Cell", inputAudio: nil),
RepeaterModel(outputText: "9th Cell", inputAudio: nil),
RepeaterModel(outputText: "10th Cell", inputAudio: nil),
RepeaterModel(outputText: "11th Cell", inputAudio: nil),
RepeaterModel(outputText: "12th Cell", inputAudio: nil)
]
self.objects += items
}
//MARK: - TableView Methods
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// 25 top spacing + 50 view element width + 25 bottom spacing
return 100.0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = self.tableView.dequeueReusableCell(withIdentifier: "Repeat") as? RepeatCell {
cell.model = objects[indexPath.row]
// other cell stuff
return cell
}
return UITableViewCell()
}
}
Pressing "Listen" then "Speak" on each cell going downward yields this output:

collectionviewcell pass data to relevant collectionview

I am using collectionview. In this collectionview I have a button and I have 2 different cells in it which is 2 different collectionviewcell. When I press the button I need to get text from its second cells. the second cell is register cell which contains username, email and password fields.
Here is code snippets.
This is LoginController
class LoginController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDelegate, UICollectionViewDataSource {
******* I am taking of cell; If I do not do that then TextField returns nil *******
var registerController: RegisterControllerCell?
override func viewDidLoad() {
super.viewDidLoad()
UIApplication.shared.statusBarStyle = .lightContent
view.backgroundColor = UIColor.rgb(61, 91, 151)
******* and here is making cell is accessible *******
registerController = RegisterControllerCell()
setupComponents()
}
final func setupComponents() -> Void {
.... components set up here
}
******* this is the collectionview that should hold cells in it *******
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.register(LoginControllerCell.self, forCellWithReuseIdentifier: loginCellId)
view.register(RegisterControllerCell.self, forCellWithReuseIdentifier: registerCellId)
view.translatesAutoresizingMaskIntoConstraints = false
view.bounces = false
view.showsHorizontalScrollIndicator = false
view.isPagingEnabled = true
view.layer.cornerRadius = 8
view.layer.masksToBounds = true
view.backgroundColor = .white
view.delegate = self
view.dataSource = self
return view
}()
#objc func hideKeyboard(){
self.view.endEditing(true)
}
********* HERE IS PROBLEM STARTS... this is the button that should return text of textfield when clicking occurs. *********
let rlButton: UIButton = {
let btn = UIButton()
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle("LOGIN", for: .normal)
btn.layer.cornerRadius = 5
btn.layer.masksToBounds = true
btn.backgroundColor = UIColor.brown
btn.addTarget(self, action: #selector(handleClick), for: .touchUpInside)
return btn
}()
#objc func handleClick(){
switch buttonState {
case .register?:
registerUser()
break
case .login?:
print("login")
break
case .none:
print("none of them")
break
}
}
********* here is should return the value *********
func registerUser() -> Void {
******** this should return a text but it is nil every time ********
let username = registerController?.username.text
print(username)
}
// collectionView stuff
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 2
}
// let colors: [UIColor] = [.black, .green]
let colors: [colorEnumerations] = [.white, .white]
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: loginCellId, for: indexPath) as? LoginControllerCell else {
fatalError("unresolved cell id")
}
if indexPath.item == 1 {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: registerCellId, for: indexPath) as? RegisterControllerCell else {
fatalError("unresolved cell id")
}
return cell
}
cell.backgroundColor = colors[indexPath.item].value
return cell
}
}
and this is the RegisterCell
class RegisterControllerCell: BaseCell {
override func setupComponents() {
registerComponents()
}
final func registerComponents() {
.... components set up
}
let holder: UIView = {
let vw = UIView()
vw.backgroundColor = .white
vw.translatesAutoresizingMaskIntoConstraints = false
return vw
}()
let seperator: UIView = {
let spr = UIView()
spr.translatesAutoresizingMaskIntoConstraints = false
spr.layer.borderWidth = 0.5
spr.layer.borderColor = UIColor.gray.cgColor
spr.backgroundColor = UIColor.gray
return spr
}()
let seperator2: UIView = {
let spr = UIView()
spr.translatesAutoresizingMaskIntoConstraints = false
spr.layer.borderWidth = 0.5
spr.layer.borderColor = UIColor.gray.cgColor
spr.backgroundColor = UIColor.gray
return spr
}()
let seperator3: UIView = {
let spr = UIView()
spr.translatesAutoresizingMaskIntoConstraints = false
spr.layer.borderWidth = 0.5
spr.layer.borderColor = UIColor.gray.cgColor
spr.backgroundColor = UIColor.gray
return spr
}()
******** I need to get its and others texts ********
let username: UITextField = {
let input = UITextField()
input.translatesAutoresizingMaskIntoConstraints = false
input.placeholder = "Username"
return input
}()
let email: UITextField = {
let input = UITextField()
input.translatesAutoresizingMaskIntoConstraints = false
input.placeholder = "Email"
return input
}()
let password: UITextField = {
let input = UITextField()
input.translatesAutoresizingMaskIntoConstraints = false
input.placeholder = "Password"
input.isSecureTextEntry = true
return input
}()
}
Thanks for your helps guys, I really need that.
A little not: Textfield is not nil by the way, the nil thing is its text, it is every time returns nil.

Resources