Making a new UITableView cell's textField the firstResponder - swift

I currently have a table view with cells that contain a label and a textfield, I also have a + bar button item that adds new cells.
What I hope to accomplish is when the user presses the + button the new cell is created and the text field of this cell would automatically become first responder.
Below is my current code for creating the new entry:
func newNoteline() {
let entityDescription = NSEntityDescription.entity(forEntityName: "NotebookContentEntity", in: context)
let item = NotebookContentEntity(entity: entityDescription!, insertInto: context)
item.notebookEntry = ""
item.timeOfEntry = timeOutlet.text
do {
try context.save()
} catch {
print(error)
return
}
loadNotelines()
}
I have thought of several ways of trying to solve this but without much luck of making them work including using a .tag on the text field as soon as it's made - using the text field delegate or using the tableView delegate method - indexPathForPreferredFocusedView.
I just can't figure out how to force the focus to a specific textfield within a cell without the user tapping the text field. Any thoughts?

Calling textField.becomeFirstResponder() method should do what you are looking for.
When to call this function is up to you. for e.g. in below code at cellForRowAt I checked if the value is empty then make the current textfield first responder.
class Test: UIViewController{
var myView: TestView{return view as! TestView}
unowned var tableView: UITableView {return myView.tableView}
unowned var button: UIButton {return myView.button}
var list = [String]()
override func loadView() {
view = TestView()
}
override func viewDidLoad() {
super.viewDidLoad()
for i in 1...10{
list.append("Test \(i)")
}
tableView.dataSource = self
button.addTarget(self, action: #selector(didSelect(_:)), for: .touchUpInside)
}
#objc func didSelect(_ sender: UIButton){
let indexPath = IndexPath(row: list.count, section: 0)
list.append("")
tableView.insertRows(at: [indexPath], with: .automatic)
tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
extension Test: UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TestViewCell", for: indexPath) as! TestViewCell
let value = list[indexPath.row]
cell.textField.text = value
if value.isEmpty{
cell.textField.becomeFirstResponder()
}
return cell
}
}

Related

UITableView Not Showing Reorder Controls

When I use trailingSwipeActionsConfigurationForRowAt my TableView will show the delete and reorder options, however when selecting reorder nothing happens. I think I have all of the correct methods and am calling setEditing; is there anything else I'm missing? Thanks!
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
}
func setupTableView() {
tableView.frame = self.view.frame
tableView.dataSource = self
tableView.delegate = self
tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
tableView.dragInteractionEnabled = true
self.view.addSubview(tableView)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 8
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.backgroundColor = .gray
cell.showsReorderControl = true
return cell
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
self.tableView.setEditing(editing, animated: animated)
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .normal, title: "delete") { (action, view, completion) in
tableView.reloadData()
completion(true)
}
let reorderAction = UIContextualAction(style: .normal, title: "reorder") { (action, view, completion) in
tableView.setEditing(true, animated: true)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction, reorderAction])
}
}
class CustomCell: UITableViewCell {
}
Result after swiping:
After selecting reorder:
A few observations:
You are not going to get the reorder controls if you do not implement tableView(_:moveRowAt:to:), e.g., assuming you had a model which was an array called objects, you could do the following:
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let object = objects.remove(at: sourceIndexPath.row)
objects.insert(object, at: destinationIndexPath.row)
}
The trailingSwipeActionsConfigurationForRowAt is probably not the right place to put a “reorder” command. Part of the reason is that once the table view is in edit mode and you tap on the ⛔️, the trailing actions show up, and “reorder” does not make sense in that context. E.g., here I am tapping on ⛔️ and I see the confusing actions.
I would suggest only adding “delete” as the trailing action. That way, you (a) get only “delete” if you tap on ⛔️ in isEditing mode, but also (b) get the stand-alone swipe action, too.
You cannot initiate isEditing from the trailing swipe actions (and, as discussed above, I do not think you want to, anyway). So, if you do not have “reorder” in the trailing swipe actions, you need some other method to enter edit mode. E.g., above, I added an “edit” button to the navigation bar that toggles isEditing:
#IBAction func didTapEdit(_ sender: Any) {
tableView.isEditing.toggle()
}
Then, you can keep the swipe to delete functionality, but when you tap on edit button, you have the tap on ⛔️ to delete functionality (plus the handles for reordering because we added tableView(_:moveRowAt:to:) as outlined in step one, above):
Another way to achieve reordering is to just allow drag and drop within the table view where you can long-press on a row and then drag it:
This is enabled by setting dragInteractionEnabled and dropDelegate:
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
return formatter
}()
private var objects: [Foo] = ...
override func viewDidLoad() {
super.viewDidLoad()
...
tableView.dragInteractionEnabled = true
tableView.dropDelegate = self
}
}
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource { ... }
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "delete") { [weak self] action, view, completion in
self?.objects.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .middle)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
// This is used if table view is in `isEditing` mode and by `UITableViewDropDelegate`
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let object = objects.remove(at: sourceIndexPath.row)
objects.insert(object, at: destinationIndexPath.row)
}
}
// MARK: - UITableViewDropDelegate
extension ViewController: UITableViewDropDelegate {
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
guard
session.items.count == 1, // Accept only one drag item ...
tableView.hasActiveDrag // ... from within this table view
else {
return UITableViewDropProposal(operation: .cancel)
}
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
guard let destinationIndexPath = coordinator.destinationIndexPath else { return }
for item in coordinator.items {
if let sourceIndexPath = item.sourceIndexPath {
DispatchQueue.main.async {
tableView.moveRow(at: sourceIndexPath, to: destinationIndexPath)
}
}
}
}
}
Clearly, if you were going to enable drag from this app to others, you would add UITableViewDragDelegate conformance here, and make your model objects conform to NSItemProviderReading and NSItemProviderWriting. But the above should be sufficient for dragging and dropping to reorder within a UITableView.

UITextField can't become first responder in UITableView

Context
Basic list. When user press '+', code creates a new item with default text that user can change.
Problem
I want to focus the new item as soon as user press '+' so that user can type desired name. I try to achieve this with this code:
func focus() {
title.becomeFirstResponder()
title.selectAll(nil)
}
But becomeFirstResponder() always returns false.
How can I give focus to UITextField in my UITableviewCell after creation ?
Here is full code of UITableViewController for reference:
import UIKit
class ViewController: UITableViewController{
var model: [String] = ["Existing item"]
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemUI", for: indexPath) as! ItemUI
cell.update(with: model[indexPath.item])
return cell
}
#IBAction func createItem(_ sender: UIBarButtonItem) {
let indexPath = IndexPath(item: model.count, section: 0)
model.append("new item")
tableView.reloadData()
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
let cell = self.tableView(self.tableView, cellForRowAt: indexPath) as! ItemUI
cell.focus()
}
}
}
class ItemUI: UITableViewCell{
#IBOutlet var title: UITextField!
func update(with: String) {
title.text = with
}
func focus() {
title.becomeFirstResponder()
title.selectAll(nil)
}
}
Ok I found the problem!
I was using method self.tableView(self.tableView, cellForRowAt: indexPath) instead of self.tableView.cellForRow(at: indexPath).
Here is what the documentation has to say:
"Never call this method yourself. If you want to retrieve cells from your table, call the table view's cellForRow(at:) method instead."
You can add a boolean in your view controller to keep track of adding item i.e isAddingItem with default value false and when you add new item simply update isAddingItem to true. In tableview cellForRowAt method check for last cell of tableview and if isAddingItem is true then selected all text of textfield and make it first responder.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemUI", for: indexPath) as! ItemUI
cell.update(with: model[indexPath.item])
if isAddingItem && indexPath.item == model.count {
// make your textfield first here and select text here
}
return cell
}
Also check if you have set textfield delegate.

Update Specific Button Image in UITableView Cell

I am trying to add an action to my "like" button. So that when the user taps the heart UIButton in a cell, the heart in the cell they tapped updates to a pink heart showing that they liked it. But instead it likes the heart they tapped and another random heart in a different cell that they did not interact with. I have been on this all day and any help would be grateful. For Example, if I like/tap my heart UIButton the buttons image I tapped updates, but when I scroll down another random heart updates from that same first cell button tap.
Also When I scroll and the cell leaves view and scroll back up the image returns back to unlike and other like buttons become liked.
Keep a data model for your buttons state
Try with the below code
struct TableModel {
var isLiked: Bool
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var dataSource: [TableModel] = []
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .light
dataSource = Array(repeating: TableModel(isLiked: false), count: 20)
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.showsVerticalScrollIndicator = false
self.tableView.reloadData()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dataSource.count
}
#objc func buttonSelected(_ sender: UIButton) {
dataSource[sender.tag].isLiked = !dataSource[sender.tag].isLiked
let indexPath = IndexPath(row: sender.tag, section: 0)
tableView.reloadRows(at: [indexPath], with: .automatic)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.likeBtn.tag = indexPath.row
cell.likeBtn.addTarget(self, action: #selector(buttonSelected(_:)), for: .touchUpInside)
let isLiked = dataSource[indexPath.row].isLiked
if isLiked {
cell.likeBtn.setImage(UIImage(named: "liked"), for: UIControl.State.normal)
} else {
//set unlike image
}
return cell
}
}
Currently, you have a hardcoded number of rows, but anyway you will need to have a data source with data models. When you press the button, you have to save the state of the button of a specific row. I would recommend you create a model first.
Here I provided an easy (but flexible enough) way how to do this. I haven't debugged it, but it should work and you can see the idea. I hope this would be helpful.
Create Cell Model
struct CellViewModel {
let title: String
var isLiked: Bool
// Add other properties you need for the cell, image, etc.
}
Update cell class
It's better to handle top action right in the cell class. To handle this action on the controller you can closure or delegate like I did.
// Create a delegate protocol
protocol TableViewCellDelegate: AnyObject {
func didSelectLikeButton(isLiked: Bool, forCell cell: TableViewCell)
}
class TableViewCell: UITableViewCell {
// add a delegate property
weak var delegate: TableViewCellDelegate?
#IBOutlet var titleTxt: UILabel!
#IBOutlet var likeBtn: UIButton!
//...
override func awakeFromNib() {
super.awakeFromNib()
// You can add target here or an action in the Storyboard/Xib
likeBtn.addTarget(self, action: #selector(likeButtonSelected), for: .touchUpInside)
}
/// Method to update state of the cell
func update(with model: CellViewModel) {
titleTxt.text = model.title
likeBtn.isSelected = model.isLiked
// To use `isSelected` you need to set different images for normal state and for selected state
}
#objc private func likeButtonSelected(_ sender: UIButton) {
sender.isSelected.toggle()
delegate?.didSelectLikeButton(isLiked: sender.isSelected, forCell: self)
}
}
Add an array of models and use it
This is an updated class of ViewController with usage of models.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// Provide a list of all models (cells)
private var cellModels: [CellViewModel] = [
CellViewModel(title: "Title 1", isLiked: false),
CellViewModel(title: "Title 2", isLiked: true),
CellViewModel(title: "Title 3", isLiked: false)
]
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .light
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.showsVerticalScrollIndicator = false
self.tableView.reloadData()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// return count of cell models
return cellModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
let model = cellModels[indexPath.row]
// call a single method to update the cell UI
cell.update(with: model)
// and you need to set delegate in order to handle the like button selection
cell.delegate = self
return cell
}
}
extension ViewController: TableViewCellDelegate {
func didSelectLikeButton(isLiked: Bool, forCell cell: TableViewCell) {
// get an indexPath of the cell which call this method
guard let indexPath = tableView.indexPath(for: cell) else {
return
}
// get the model by row
var model = cellModels[indexPath.row]
// save the updated state of the button into the cell model
model.isLiked = isLiked
// and set the model back to the array, since we use struct
cellModels[indexPath.row] = model
}
}

How to add selected indexes of tableview sections to an array

I have a tableView with section headers and I want to append a certain user input to multiple selected headers. I have created the section headers and am able to change the image on the section header to show that it is selected but I want to create an array of those selected.
Here is my code for the section header:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let userModel = Data.userModels[section]
let cell = tableView.dequeueReusableCell(withIdentifier: "nameCell") as! NameHeaderTableViewCell
cell.setup(model: userModel)
cell.checkMarkButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
cell.enable(on: false)
if isUserEditing == true {
cell.enable(on: true)
}
return cell.contentView
}
Here is where I change the section image when a user taps the section:
#objc func handleTap(sender: UIButton) {
sender.isSelected = !sender.isSelected
}
When the user clicks the save button, I want the user input to be appended to those cells where the section header is selected. Here is that code:
#IBAction func save(_ sender: Any) {
//VALIDATION
guard mealItemTextField.text != "", let item = mealItemTextField.text else {
mealItemTextField.placeholder = "please enter an item"
mealItemTextField.layer.borderWidth = 1
mealItemTextField.layer.borderColor = UIColor.red.cgColor
return
}
guard priceTextField.text != "", let price = Double(priceTextField.text!) else {
priceTextField.placeholder = "please enter a price"
priceTextField.layer.borderWidth = 1
priceTextField.layer.borderColor = UIColor.red.cgColor
return
}
tableView.reloadData()
}
Currently I am stuck on how to access all of the indexes of the sections that are selected(i.e. the ones that have a state of selected) Here are some screenshots to help visualize the program:
P.S. Not sure if this is helpful but I populate the data with an array of structs. Here is that code:
func numberOfSections(in tableView: UITableView) -> Int {
return Data.userModels.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if Data.userModels[section].isExpandable {
return Data.userModels[section].itemModels.count
} else {
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
}
cell?.textLabel?.text = Data.itemModels[indexPath.row].itemName
return cell!
}
Any help is appreciated, thanks!
The problem is that your checkmarks are merely a visual feature of the header. When the user taps on a checkmark, you're merely toggling its selected state:
#objc func handleTap(sender: UIButton) {
sender.isSelected = !sender.isSelected
}
That's not going to work. You need to be keeping track of this information at all times in a part of your data model that deals with the section headers. That way, when the Save button is clicked, the information is sitting there in the data model waiting for you. Your handleTap needs to work out what section this is the header of and reflect the info off into the model. The data model is the source of truth, not some view in the interface. (I am surprised that you are not already having trouble with this when you scroll your table view.)
Another problem with your code is this:
let cell = tableView.dequeueReusableCell(withIdentifier: "nameCell") as! NameHeaderTableViewCell
You can't use a UITableViewCell as a reusable section header view. You need to be using a UITableViewHeaderFooterView here.

Can't get indexPath of cell in header

I have created prototype custom header cell for a tableView with a button on it. I am trying to get the indexPath of the cell when the button is tapped, but I don't receive it. Any idea what I am doing wrong here?
protocol MediaHeaderCellDelegate: class {
func editPost(cell: MediaHeaderCell)
}
class MediaHeaderCell: UITableViewCell {
weak var delegate: MediaHeaderCellDelegate?
#IBAction func moreOptionsAction(_ sender: UIButton) {
delegate?.editPost(cell: self)
}
}
class NewsfeedTableViewController:UITableViewController, MediaHeaderCellDelegate {
func editPost(cell: MediaHeaderCell) {
guard let indexPath = tableView.indexPath(for: cell) else {
print("indexpath could not be given")
return}
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
let cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.mediaHeaderCell) as! MediaHeaderCell
cell.delegate = self
cell.media = media[section]
return cell
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.mediaCell, for: indexPath) as! MediaTableViewCell
cell.currentUser = currentUser
cell.media = media[indexPath.section]
cell.delegate = self
return cell
}
}
So this is actually all about learning what section a section header belongs to?? Here’s what I do. I have a header class:
class MyHeaderView : UITableViewHeaderFooterView {
var section = 0
}
I register it:
self.tableView.register(
MyHeaderView.self, forHeaderFooterViewReuseIdentifier: self.headerID)
I use and configure it:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let h = tableView
.dequeueReusableHeaderFooterView(withIdentifier: self.headerID) as! MyHeaderView
// other stuff
h.section = section // *
return h
}
Now if the header view is tappable or contains a button or whatever, learning what section this is the header of is trivial.
Your immediate issue is that you are using a table cell as a section header view. That should not be done. Once you resolve that, your next task is to determine the table section from the header view whose button was tapped.
First, change your MediaHeaderCell to be a header view that extends UITableViewHeaderFooterView and update your protocol accordingly:
protocol MediaHeaderViewDelegate: class {
func editPost(view: MediaHeaderView)
}
class MediaHeaderView: UITableViewHeaderFooterView {
weak var delegate: MediaHeaderViewDelegate?
#IBAction func moreOptionsAction(_ sender: UIButton) {
delegate?.editPost(cell: self)
}
}
Then you need to register the header view in your view controller.
Then update your viewForHeaderInSection:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: Storyboard.mediaHeaderView) as! MediaHeaderView
view.delegate = self
view.media = media[section]
view.tag = section
return view
}
And last, update your protocol method implementation:
func editPost(view: MediaHeaderView) {
let section = view.tag
// do something
}
There is one possible issue with this. If your table allows sections to be added or removed, then it is possible that a header view's tag could be wrong when the button is tapped.