UITableView Cell Does Not Become First Responder When Outside of Current View - ios

I have a TableViewController which displays to-do list items. In the controller I have made a button which when pressed creates a new TableViewCell at the bottom which has a UITextView along with other elements.
Till now this is what I have managed to do -
Create a new cell upon button tap
Make the newly created cell's text view first responder
However, from what I have observed everything is working fine except when the last cell in the table is not visible, i.e., it is below the frame. In that case the cell gets created but is not made the first responder or some other cell's text view gets the cursor.
See the output here -
https://drive.google.com/file/d/1mRN8MEO5HBJ3ICUiRE0Yc4ib8tp62MYc/view?usp=sharing
Here is the code -
import UIKit
class TableViewController: UITableViewController, InboxCellDelegate {
var cell = InboxCell()
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.leftBarButtonItem = self.editButtonItem
tableView.register(UINib(nibName: "InboxCell", bundle: nil), forCellReuseIdentifier: "InboxCell")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 50
}
#IBAction func inboxAddPressed(_ sender: UIBarButtonItem) {
addRowToEnd()
}
func addRowToEnd() {
Task.tasks.append("")
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: Task.tasks.count - 1, section: 0)], with: .automatic)
tableView.endUpdates()
cell.inboxTaskTextView.becomeFirstResponder()
}
func didChangeText(text: String?, cell: InboxCell) {
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return Task.tasks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
cell = tableView.dequeueReusableCell(withIdentifier: "InboxCell", for: indexPath) as! InboxCell
// Configure the cell...
cell.delegate = self
cell.inboxTaskTextView.text = Task.tasks[indexPath.row]
return cell
}
// Override to support conditional editing of the table view.
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
tableView.beginUpdates()
Task.tasks.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
I have tried to scroll to the bottom of the table first and then making the newly created cell first responder but that didn't work. In that case only the very first cell created becomes the first responder while the subsequent cells are created but the cursor remains in the very first cell.
Here is the block of code I used for scrolling before cell.inboxTaskTextView.becomeFirstResponder() -
DispatchQueue.main.async {
self.tableView.reloadData()
let indexPath = IndexPath(row: Task.tasks.count - 1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
Edit -
After having tried for a while this is the closest I have got to a solution.
After Task.tasks.append("") I have added the following code which scrolls down the view to the bottom -
if tableView.contentSize.height >= tableView.frame.size.height {
DispatchQueue.main.async {
self.tableView.reloadData()
let indexPath = IndexPath(row: Task.tasks.count - 1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
In this case the newly created cell becomes first responder but only momentarily. The keyboard doesn't even appear fully before it gets dismissed automatically in a flash. This happens only for cells that are created below the fold - i.e. when the table view has to scroll down and then create a new cell.

Try to keep it simple first. Put a breakpoint or print("indexPath.row = \(indexPath.row)") at the beginning of cellForRow UITableViewDataSource method that you implemented already.
Add the new task and see if your cellForRow is being called for the indexPath corresponding to your new cell.
If not - you may have to scroll the tableView up at least 44points or whatever is needed to reach the area where the new cell should already be displayed. If you don't do that - the cell might not be created yet, and cell most probably refers to the last cell in the table view (or it could also be referring to a cell in the pool if some logic is not implemented correctly). So the new cell must be in the visible area before making its UITextField or UITextView become first responder.
If you know that cell is already visible - better to get an reference to it via
let index = IndexPath(row: rowForNewCell, section: 0)
let cell = self.table.cellForRow(at: index)
Finally call:
cell.inboxTaskTextView.becomeFirstResponder()

Related

Table View insetGrouped Bug

I have expandable table view cells, everything works, except animation. I have one cell with UISwitch, when I tap on it, other cells appear but with some view movements, same thing when I hide these cells when I tap on UISwitch.
I'm using insetGroup Table View and these movements make view square instead of round.
I want to keep animation, I tried reloadSections(IndexSet(integer: 0), with: .fade) seems like a solution, but it also reloads my header, but I don't want that.
My cellForRowAt func:
internal func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RemindersCell", for: indexPath) as! RemindersCell
cell.switchReminder.isOn = remindersAllowed ? true : false
cell.switchReminder.addTarget(self, action: #selector(switchTapped(sender:)), for: .touchUpInside)
return cell
}
My numberOfRowsInSection func:
internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return remindersAllowed ? 5 : 1
}
My func for UISwitch:
#objc private func switchTapped(sender: UISwitch) {
if !remindersAllowed {
// Add
remindersAllowed = true
} else {
// Delete
remindersAllowed = false
}
tableView.beginUpdates()
tableView.reloadSections(IndexSet(integer: 0), with: .none)
tableView.endUpdates()
}
Default remindersAllowed is true, when I switch it becomes false and hides cells. I really don't understand what the problem is, any help would be appreciated!
The gif shows this bug when I hide the cells.
you can use activate cell as a tableviewheader
You can try this code to delete system reload section animation and reload section with fade animation:
tableView.reloadData()
tableView.reloadSections(IndexSet(integer: 0), with: .fade)

When deleting row using trailingSwipeActionsConfigurationForRowAt strange missing other rows issue

I am trying to delete row from UITableView by using trailingSwipeActionsConfigurationForRowAt function.
The row gets deleted and disappears. This part is ok.
But next rows that are coming to the screen or when I swipe down the same amount as the rows been deleted are not even loaded on the tableView.
![img1]https://poirot.deus4.com/photo_2019-06-12_16-44-01.jpg
![img2]https://poirot.deus4.com/photo_2019-06-12_16-43-56.jpg
![img3]https://poirot.deus4.com/photo_2019-06-12_16-43-49.jpg
![img4]https://poirot.deus4.com/photo_2019-06-12_16-43-38.jpg
[video]https://poirot.deus4.com/RPReplay_Final1560345600.mp4
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredProducts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: .cellID, for: indexPath) as! ProductTableCell
cell.backgroundColor = .red
return cell
}
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let hide = UIContextualAction(style: .destructive, title: "Hide") { action, view, completion in
self.filteredProducts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
hide.image = #imageLiteral(resourceName: "hdie_product")
hide.backgroundColor = .midGrey
let conf = UISwipeActionsConfiguration(actions: [hide])
return conf
}
The code you showed obviously works, thus the problem is elsewhere.
Without other code I suspect that the problem is because table view cells are re-used: When you scroll your table view, some cells are scrolled out of view and can be reused. Cells that are scrolled in are either re-used cells or new cells.
In any case, the table view datasource function tableView(_:cellForRowAt:) must be used to configure the displayed cells.
It seems too me that you do not configure the cells in tableView(_:cellForRowAt:).
If so, re-used cells look as the were when they have been scrolled out, but new cells are simply blank.
So I suggest to check if you really configure all the cells in tableView(_:cellForRowAt:) correctly.
The issue was that I did not call super.prepareForReuse() inside TablewViewCell
override func prepareForReuse() {
super.prepareForReuse() // this was missing
}

Table view cell is remain unselected in editing mood

I want even numbered rows of tableview to be selected when table is first loaded. I have set the tableview editing style to "Multiple Selection During Editing" from storyboard. I have tried two ways to do this, first approach is like below
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.setEditing(true, animated: true)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
cell?.textLabel?.text = data[indexPath.row]
if (indexPath.row % 2 == 0){
cell?.isSelected = true
}else {
cell?.isSelected = false
}
return cell!
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
but it does not make the row selected. table looks like
second way i tried is to call self.tableView.selectRow(at: index path, animated: true, scrollPosition: UITableViewScrollPosition.none) whith removing additional logic in cellForRow method used in first approach. this approach seems to work.
Can anyone tell me why first approach is not working and weather my second approach is appropriate or not. Thanks :)
Check if you are doing deselectRowAtIndexPath in didSelectRowAtIndexPath method.

Swift What is the best way to replace content in TableViewCell

I want to do something like in the GIF
I tried 2 ways, one was hiding the elements on selecting the row and showing others, but that's not really elegant and doesn't work very well
and second was creating 2 views, one with labels, another with buttons, adding them as subviews to cell.contentView but that caused some issues with other cells as they were displaying wrong data. How can I recreate something like this?
I think something like this would work:
Use 2 different UITableViewCells: add them to the table view in your storyboard and design them separately, also you can use 2 different UITableViewCell subclasses for them
Have an array in the tableview's datasource class that will define the type of the cell from each row (e.g. the simplest solution would be an array of integers, with values: 0 representing the first cell, 1 representing the second cell)
Initialise that array with 0s for each row
In tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell :
if cellTypes[indexPath.row] == 0 --> return a cell of first type
if cellTypes[indexPath.row] == 1 --> return a cell of the second type
In tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) :
switch the cell type in the array
reload the row with animation, e.g. you can use .fade or .left or .right etc.
tableView.reloadRows(at: [indexPath], with: .fade)
EDIT: Your solution is also a good one, but it can cause problems when the cells are dequeued, so if a cell with the wrong subviews is dequeued then you need to switch the subviews back in the cellForRowAt indexPath datasource method.
EDIT2: I took some time and I have tried my solution in Xcode. Here is the code of my tableview controller:
class TableViewController: UITableViewController {
private var cellTypes: [Int] = [1, 1, 1, 1, 1, 1]
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.cellTypes.count
}
public override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 56.0
}
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if self.cellTypes[indexPath.row] == 1 {
return tableView.dequeueReusableCell(withIdentifier: "Cell1", for: indexPath)[![enter image description here][1]][1]
} else {
return tableView.dequeueReusableCell(withIdentifier: "Cell2", for: indexPath)
}
}
public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if self.cellTypes[indexPath.row] == 1 {
self.cellTypes[indexPath.row] = 2
tableView.reloadRows(at: [indexPath], with: .fade)
} else {
self.cellTypes[indexPath.row] = 1
tableView.reloadRows(at: [indexPath], with: .right)
}
}
}
And here is how it is working in the iOS simulator:
I believe you are on the right track about creating 2 separate views inside the cell; one for showing 3 buttons (Play Now, Play Next etc.) and, one for showing the song's details (song name, singer name etc.).
In order not to mess with frames or constraints (in case you are using Autolayout), the main trick here is to create a snapshot of the view containing the buttons and move it to the end of the cell.
As I said above, you should have 2 separate views. I'll call them:
infoView: View that has 2 labels showing the song's and the singer's name.
actionsView: View that has 3 buttons for play actions. (Now, Next, Last etc.)
Here are things that you should do when user taps on a cell:
Check if cell is not selected. If it is not, then hide infoView and show actionView.
If cell is selected:
Deselect the cell.
Create a snapshot out of actionsView, set its frame accordingly so it'll shadow the real actionsView.
Set actionView's isHidden property to true.
Set infoView's isHidden property to false.
Set frame.origin.x value of the snapshot to contentView's maxX in an animation block so it'll move to the right side of the cell smoothly.
At the end of the animation, remove the snapshot from view hierarchy.
I've created a cell class and defined a method that executes those steps:
public class SongCell: UITableViewCell {
#IBOutlet fileprivate weak var infoView: UIView!
#IBOutlet fileprivate weak var actionsView: UIView!
...
public func showActions(_ show: Bool) {
switch show {
case true:
infoView.isHidden = true
actionsView.isHidden = false
case false:
if let snapshot = actionsView.snapshotView(afterScreenUpdates: true) {
snapshot.frame = actionsView.frame
contentView.addSubview(snapshot)
actionsView.isHidden = true
infoView.isHidden = false
UIView.animate(withDuration: 0.25, animations: {
snapshot.frame.origin.x = self.contentView.frame.maxX
}, completion: { _ in
snapshot.removeFromSuperview()
})
}
else {
infoView.isHidden = false
actionsView.isHidden = true
}
}
}
}
Here is how it looks on my simulator:
You can download the project from here.

Why do collapsing/expandable iOS UITableView rows disappear on interaction?

I am new to iOS Development and I just implemented a simple expandable sections UITableView. I am not able to understand why some rows disappear and sometimes change position when the row heights are recalculated on tapping the section header. I went through all the already answered questions on this topic and have not been able to find the right solution.
Following is a scenario:
Launch the app:
Tap on the section header:
Section expands
All other headers disappear
Tap again
Section collapses
The headers continue to be blank
Scrolled to the bottom and back to the top
The positions of headers changed
Scrolled to the bottom and back to the top again
The positions of headers changed again with some cells still blank
Things I have already tried:
Wrapping reloadRowsAtIndexPaths in updates block (beginUpdates() and endUpdates())
Using reloadRowsAtIndexPaths with animation set to .none
Removing reloadRowsAtIndexPaths at all while keeping the updates block
Using reloadData() instead which actually works but I lose animation
Code:
Here is the link to the project repository.
You're using cells for the header. You shouldn't do that, you need a regular UIView there, or at least a cell that's not being dequeued like that. There's a few warnings when you run it that give that away. Usually just make a standalone xib with the view and then have a static method like this in your header class. Make sure you tie your outlets to the view itself, and NOT the owner:
static func view() -> HeaderView {
return Bundle.main.loadNibNamed("HeaderView", owner: nil, options: nil)![0] as! HeaderView
}
You're reloading the cells in the section that grows, but when you change the section that's grown you'd need to at least reload the former section for it to take the changes to it's cell's height. You can reload the section by index instead of individual rows in both cases
Ok as you ask, I am changing my answer according to you.
import UIKit
class MyTableViewController: UITableViewController {
let rows = 2
var categories = [Int](repeating: 0, count: 10)
struct Constants {
static let noSelectedSection = -1
}
var selectedSection: Int = Constants.noSelectedSection
func selectedChanged(to selected: Int?) {
let oldIndex = selectedSection;
if let s = selected {
if selectedSection != s {
selectedSection = s
} else {
selectedSection = Constants.noSelectedSection
}
tableView.beginUpdates()
if(oldIndex != -1){
tableView.reloadSections([oldIndex,s], with: .automatic)
}else{
tableView.reloadSections([s], with: .automatic)
}
tableView.endUpdates()
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return categories.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("reloading section \(section)")
return (selectedSection == section) ? rows : 0;//rows
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return tableView.rowHeight
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return tableView.rowHeight
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "Header")
if let categoryCell = cell as? MyTableViewCell {
categoryCell.category = section + 1
let recognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
recognizer.numberOfTapsRequired = 1
recognizer.numberOfTouchesRequired = 1
categoryCell.contentView.tag = section;
categoryCell.contentView.addGestureRecognizer(recognizer)
}
return cell?.contentView
}
func handleTapGesture(recognizer: UITapGestureRecognizer) {
if let sindex = recognizer.view?.tag {
selectedChanged(to: sindex)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Body", for: indexPath)
if let label = cell.viewWithTag(1) as? UILabel {
label.text = "Body \(indexPath.section + 1) - \(indexPath.row + 1)"
}
return cell
}
}
As you can see now I am just reloading a particular section instead of reloading the whole table.
also, I have removed gesture recognizer from the cell & put this into the main controller.

Resources