Unexpected behaviour here.
I have a chat composed of a tableview and a bottom bar with a textView.
When the user select the textView the keyboard appears.
I use the notification UIKeyboardWillShowNotification to modify the frame of the tableview.
Selecting a row display a UIAlertController with different actions removing the keyboard.
This UIAlertController has a cancel action which is removing the selection of the cell (by selection i mean the grey style appearing when the selected state of the cell is true).
The selection is removed by setting cell.selected to false.
Problem is, when the keyboard appears, this previously selected cell is selected again, i tried to loop through the cells to put them all at selected false but depending on where the cell is located, the selected state will only appear after scrolling, making my loop uneffective.
Since i don't select the cells manually in my code, i assume this is a behaviour resulting maybe from the change of frame?
Is there something i can do to fix this?
Create an indexPath variable for referring to it later
var indexPath: IndexPath?
Inside tableView delegate didSelect Row assign the selected indexPath to the one you have created
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.indexPath = indexPath
}
Inside the function where you're listening for keyboard notification deselect the selected row using your referenced indexPath
func keyboardDidShow(notification: Notification) {
// your existing code...
if let indexPath = indexPath {
// deselect the selected row. I set animation to false
tableView.deselectRow(at: indexPath, animated: false)
// just a clean up set the indexPath to nil once done
self.indexPath = nil
}
}
This was a one way to fix the problem.
Each Cell has a selectedView you can set that Views backgroundColor to clearColor then the row will be selected but you won't see that. or you can return your own view for the selectedBackgroundView.
Adding a bit of code will help a lot.
With limited info, this is what I feel:
It may be due to reuse of a dequeued cell (since you make changes to the table when the keyboard notification is triggered).
This is my general practice:
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: CELL_REUSE_IDENTIFIER)
prepareDequeuedCellForReuse(cell)
The prepareDequeuedCellForReuse has something like this:
private func prepareDequeuedCellForReuse(_ dequedCell: UITableViewCell){
dequedCell.textLabel?.text = ""
dequedCell.tag = -1;
dequedCell.isSelected = false
}
I recommend you clear the selection here.
Related
I have a collection view scrollable in 2-dimensions. Each row is a section with some cells.
Now in each row only one item is special (will have enum state = special, otherwise normal).
I want a behavior such that when we move up or down and we were on a special cell we should jump to special cell of next row.
View that behavior here
I did it like this, I will be storing previous focused cell's indexpath in a variable and when I move to next cell then in collectionView(:canFocusItemAt:) I will check if previous was special and we are changing the section then only next cell is focusable if it's special.
func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool {
if let previousFocusIndex = focusIndex,
previousFocusIndex.section != indexPath.section,
let previousCell = collectionView.cellForItem(at: previousFocusIndex) as? EpisodeCell,
let nextCell = collectionView.cellForItem(at: indexPath) as? EpisodeCell,
previousCell.timeState == .special {
print("🟢#Searching_Episode: \(nextCell.description)")
return nextCell.timeState == .special
}
return true
}
But now I came to know about behavior of focus engine that its search area is based on current cell's width.
And is a cell in next row does not lies in that search area it will not focus there.
Problem I am facing
I tried googling but didn't find anything better and can't think what else I can try.
If you have an idea it will be really helpful. Thanks.
There are 2 parts to solve this problem.
Identify the user is seeing the special content shown in collection view[Special Item]
This can be achieved by checking the Visible Items and cross check with data source that if it is special
var indexPathsForVisibleItems: [IndexPath] { get }
Scrolling to Special Item in a particular section
Based on the out come of step 1 we should decide calling scrollToItem
func scrollToItem(at: IndexPath, at: UICollectionView.ScrollPosition, animated: Bool)
You may put this logic when you identify that user is scrolling the collection view by confirming to UIScrollViewDelegate, which is inherited by UICollectionViewDelegate.
protocol UIScrollViewDelegate //Protocol to use
func scrollViewDidScroll(UIScrollView)
//Tells the delegate when the user scrolls the content view within the scroll view.
We need to have optimised logic here such that scrollToItem is only called when there is a new row is showed.
I am changing the numberOfLines attribute on a label that lives in a custom UITableViewCell when the cell is tapped. However, this is not reflected in the UI until the second tap. The cell is configured as a prototype cell in the table view to initially have 2 lines.
Interestingly enough, when I print out the numberOfLines value before and after my tapped() function runs, the values start off different, and then synchronize - after the first tap, I see 2 lines before the function runs, then 0 lines after the function runs. However, after subsequent taps, I see the same value before and after my function, which makes it seem like it's not doing anything, even though the UI does stretch and shrink the cell, and the numberOfLines value is changed for the next time the didSelectRowAtIndexPath function runs.
I'm only seeing this behavior with tableView.reloadRows(). If I do a full update with tableView.reloadData(), the cell appropriately grows and collapses the first time it is tapped. However, this feels a bit ham-fisted and doesn't animate nicely like reloadRows() does.
TableView Implementation
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ReviewTableViewCell
else { return }
let data = tableData[indexPath.row]
print("old number of lines: \(cell.detailLabel.numberOfLines)")
//data.isOpen is set to false initially
cell.tapped(data.isOpen)
tableData[indexPath.row].isOpen = !data.isOpen
tableView.reloadRows(at: [indexPath], with: .fade)
print("old number of lines: \(cell.detailLabel.numberOfLines)")
// tableView.reloadData()
}
Custom Table View Cell method
func tapped(_ isOpen: Bool) {
if !isOpen {
detailLabel.numberOfLines = 0 }
else {
detailLabel.numberOfLines = 2 }
}
I am expecting this code to expand the cell once it is reloaded with tableView.reloadRows() if the numberOfLines is set to 0 and collapse the cell when it is set to 2. This does work, but only after tapping the cell two+ times. This should work with the first tap as well.
Here is a link of a gif that shows the issue: https://imgur.com/a/qe2uAXj
Here is a sample project that is similar to what's going on in my app: https://github.com/imattice/CellLabelExample
Just to be clear, to get this trick work UILabel generally must be constrained on each side to it's superview, in this way when it changes its intrinsicContentSize is able to push each side to accomodate the text.
Saying that, try to wrap the tapped method with those two methods:
tableView.beginUpdates()
if !isOpen {
detailLabel.numberOfLines = 0
}
else {
detailLabel.numberOfLines = 2
}
tableView.endUpdates()
Of course tableview must be set to automatic size:
tableView.estimatedRowHeight = <#What you want#>
tableView.rowHeight = UITableView.automaticDimension
I was able to work out what was going on. The problem is in two parts.
The first part is calling reloadRows(). This method is swapping out the cells with a new cell rather than updating the cell that already exists. Therefore, I'm changing the number of lines on that hidden swap cell rather than the cell that is in view. This behavior is mentioned in the docs:
Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out.
Additionally, I'm using structs as the data model for tracking the open status of the cell. In Swift, structs are copy-on-write, which means that if a value is changed on that struct, a new struct is created rather than changing the value of that struct I'm pointing to. This means the line tableData[indexPath.row].isOpen = !data.isOpen doesn't do anything useful - we look at the tableData struct at the index path, get it's isOpen value, copy a new struct and change that new struct's isOpen value, and then throw it out because the new struct is not assigned anywhere.
The solution is to not use the reloadRows() method and to either use
A) a class for the data object
B) replace the data at indexPath.row to the copied struct
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? CustomCell else { return }
var data = tableData[indexPath.row]
tableView.beginUpdates()
cell.tapped(isOpen: data.isOpen)
data.isOpen = !data.isOpen
tableData[indexPath.row] = data
tableView.endUpdates()
}
Current Situation:
I have a UITableView with custom cells. In the cells are 1 label and 1 textview.
Scrolling is enabled for the UITableView.
Below the Table, I have a button to save the entries from the textview.
My Problem is:
I get only the value from the first row.
From the other rows, I get always an empty text, but only if the user has scrolled.
I don't understand why and I hope you can help me.
Here is my Code:
#objc func save()
{
for i in 0..<labels.count
{
let indexPath = IndexPath(item: i, serction: 0)
let cell = self.SearchTable.dequeueReusableCell(withIdentifier: "searchCell",
for: indexPath) as! SearchCell
print("Index: \(indexPath)")
if(cell.EditField.text = ""
{
continue;
}else{
...
}
}
}
Debugger
First Row: FirstRow
Other Rows: SecondRow
Cells in a tableview are reused, so a cell at an index path that is not visible could have values from a completely different cell. If you want to get all the cells that are currently visible on the screen, you could use tableView.visibleCells docs to get all the cell that are currently on screen and extract data from there.
Alternatively, you could choose to not implement cell reuse and make your table view static. You can do this in Interface Builder, or you could also choose to create all the cells up front and return your pre-made cells in tableView(_:cellForRowAt:). Note that a setup like this is okay for small datasets, but has terrible performance for larger sets so be aware that this might not be the best way to do things. It really depends on your situation. This method of doing things would end up looking a bit like this:
var cells = [UITableViewCell]()
override func viewDidLoad() {
// do all kinds of stuff
for field in fields { // or whatever else mechanism you use as your datasource
let cell = UITableViewCell()
// configure your cell
cells.append(cell)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return cells[indexPath.row]
}
The third and last way you might want to solve this is to add a delegate to your cells and set the view controller as the delegate. If the text changes, call the delegate with the cell's index path and the new text value. You can then store this text somewhere in the view controller and read it from there when you save rather than pulling it from the cell's textfield. Personally I would prefer this method.
Rather than iterate over the cells in the tableview, you could just get the data from the data source.
Whenever the text in the cell's text field changes you could update the data source and then use the information from there to perform your save.
You must be having some kind of data source anyway, otherwise what happens to the text when the cell scrolls off the screen and comes back on again? If you aren't storing the text somewhere then you've got nothing to populate the cell with in the table views cellForRow(... delegate method.
I have a table view with custom cells. They are quite tall, so only one cell is completely visible on the screen and maybe, depending on the position of that cell, the top 25% of the second one. These cells represent dummy items, which have names. Inside of each cell there is a button. When tapped for the first time, it shows a small UIView inside the cell and adds the item to an array, and being tapped for the second time, hides it and removes the item. The part of adding and removing items works fine, however, there is a problem related to showing and hiding views because of the fact that cells are reused in a UITableView
When I add the view, for example, on the first cell, on the third or fourth cell (after the cell is reused) I can still see that view.
To prevent this I've tried to loop the array of items and check their names against each cell's name label's text. I know that this method is not very efficient (what if there are thousands of them?), but I've tried it anyway.
Here is the simple code for it (checkedItems is the array of items, for which the view should be visible):
if let cell = cell as? ItemTableViewCell {
if cell.itemNameLabel.text != nil {
for item in checkedItems {
if cell.itemNameLabel.text == item.name {
cell.checkedView.isHidden = false
} else {
cell.checkedView.isHidden = true
}
}
}
This code works fine at a first glance, but after digging a bit deeper some issues show up. When I tap on the first cell to show the view, and then I tap on the second one to show the view on it, too, it works fine. However, when I tap, for example, on the first one and the third one, the view on the first cell disappears, but the item is still in the array. I suspect, that the reason is still the fact of cells being reused because, again, cells are quite big in their height so the first cell is not visible when the third one is. I've tried to use the code above inside tableView(_:,cellForRow:) and tableView(_:,willDisplay:,forRowAt:) methods but the result is the same.
So, here is the problem: I need to find an EFFICIENT way to check cells and show the view ONLY inside of those which items are in the checkedItems array.
EDITED
Here is how the cell looks with and without the view (the purple circle is the button, and the view is the orange one)
And here is the code for the button:
protocol ItemTableViewCellDelegate: class {
func cellCheckButtonDidTapped(cell: ExampleTableViewCell)
}
Inside the cell:
#IBAction func checkButtonTapped(_ sender: UIButton) {
delegate?.cellCheckButtonDidTapped(cell: self)
}
Inside the view controller (NOTE: the code here just shows and hides the view. The purpose of the code is to show how the button interacts with the table view):
extension ItemCellsTableViewController: ItemTableViewCellDelegate {
func cellCheckButtonDidTapped(cell: ItemTableViewCell) {
UIView.transition(with: cell.checkedView, duration: 0.1, options: .transitionCrossDissolve, animations: {
cell.checkedView.isHidden = !cell.checkedView.isHidden
}, completion: nil)
}
EDITED 2
Here is the full code of tableView(_ cellForRowAt:) method (I've deleted the looping part from the question to make it clear what was the method initially doing). The item property on the cell just sets the name of the item (itemNameLabel's text).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
ItemTableViewCell.identifier, for: indexPath) as? ItemTableViewCell{
cell.item = items[indexPath.row]
cell.delegate = self
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
I've tried the solution, suggested here, but this doesn't work for me.
If you have faced with such a problem and know how to solve it, I would appreciate your help and suggestions very much.
Try this.
Define Globally : var arrIndexPaths = NSMutableArray()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = self.tblVW.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.textLabel?.text = String.init(format: "Row %d", indexPath.row)
cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: #selector(btnTapped), for: .touchUpInside)
if arrIndexPaths.contains(indexPath) {
cell.backgroundColor = UIColor.red.withAlphaComponent(0.2)
}
else {
cell.backgroundColor = UIColor.white
}
return cell;
}
#IBAction func btnTapped(_ sender: UIButton) {
let selectedIndexPath = NSIndexPath.init(row: sender.tag, section: 0)
// IF YOU WANT TO SHOW SINGLE SELECTED VIEW AT A TIME THAN TRY THIS
arrIndexPaths.removeAllObjects()
arrIndexPaths.add(selectedIndexPath)
self.tblVW.reloadData()
}
I would keep the state of your individual cells as part of the modeldata that lies behind every cell.
I assume that you have an array of model objects that you use when populating you tableview in tableView(_:,cellForRow:). That model is populated from some backend service that gives you some JSON, which you then map to model objects once the view is loaded the first time.
If you add a property to your model objects indicating whether the cell has been pressed or not, you can use that when you populate your cell.
You should probably create a "wrapper object" containing your original JSON data and then a variable containing the state, lets call it isHidden. You can either use a Bool value or you can use an enum if you're up for it. Here is an example using just a Bool
struct MyWrappedModel {
var yourJSONDataHere: YourModelType
var isHidden = true
init(yourJSONModel: YourModelType) {
self.yourJSONDataHere = yourJSONModel
}
}
In any case, when your cell is tapped (in didSelectRow) you would:
find the right MyWrappedModel object in your array of wrapped modeldata objects based on the indexpath
toggle the isHidden value on that
reload your affected row in the table view with reloadRows(at:with:)
In tableView(_:,cellForRow:) you can now check if isHidden and do some rendering based on that:
...//fetch the modelObject for the current IndexPath
cell.checkedView.isHidden = modelObject.isHidden
Futhermore, know that the method prepareForReuse exists on a UITableViewCell. This method is called when ever a cell is just about to be recycled. That means that you can use that as a last resort to "initialize" your table view cells before they are rendered. So in your case you could hide the checkedView as a default.
If you do this, you no longer have to use an array to keep track of which cells have been tapped. The modeldata it self knows what state it holds and is completely independent of cell positions and recycling.
Hope this helps.
I have a UITableView with custom cells. Usually when the user taps the cell, it triggers this function:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//Segue to another view
}
If in that view they mark the cell as finished, when they return the code will add the standard check accessory.
When the cell isn't finished, I'd like an empty check that can be tapped (I know I can add a custom image accessory), with the tap allowing the user to skip the segue and quickly mark the cell as finished. But I can't seem to make both work:
Tapping on the main body of the cell should call didSelectRowAtIndexPath
Tapping on the accessory view (empty check mark) should call a different set of code.
I've tried accessoryButtonTappedForRowWithIndexPath but it doesn't seem to even get called.
func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
//Shortcut code to change the cell to "finished"
}
Is it possible to have clicking on the main body trigger one set of code and clicking on the accessory view trigger another? If so, how?
You should add a UIButton to your custom UITableViewCell. You can then add a target for that button called pressedFinished, by saying something like cell.button.addTarget(self, action: "finishedPress:", forControlEvents: .TouchUpInside)
or something then in pressedFinished you can say something like:
func pressedFinished(sender:UIButton)
{
let location = self.tableView.convertPoint(sender.bounds.origin, fromView: sender)
let indexPath = self.tableView.indexPathForRowAtPoint(location)
//update your model to reflect task at indexPath is finished and reloadData
}
It's generally not a great practice to use tags as they have no inherent meaning. Also tags mapped to indexPath.row will only work if the table has one section.
Another option might be using UITableViewRowAction:
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [AnyObject]? {
let rowAction : UITableViewRowAction
if model[indexPath].finished
{
rowAction = UITableViewRowAction(style: .Normal, title: "Mark as Unfinished", handler: {(rowAction:UITableViewRowAction,indexPath:NSIndexPath)->() in
//mark as unfinished in model and reload cell
})
}else{
rowAction = UITableViewRowAction(style: .Normal, title: "Mark as Finished", handler: {(rowAction:UITableViewRowAction,indexPath:NSIndexPath)->() in
//mark as finished in model and reload cell
})
}
return [rowAction]
}
Its a Objective-C solution.
When you have added your own accessoryButton, that won't call tableviewDelegate method of 'accessoryButtonTappedForRowWithIndexPath'.
What you should do is, create a UIButton and addTarget to that method, then add it as a accessoryButton on tableViewCell. Also set tag value to button as index path.row so that you get to know which row is clicked.
I selected the answer that I thought gave a thorough answer, and which helped me out. However, I deviated from that answer a bit and wanted to share this as well:
Making The Accessory View Appear / Disappear
I load several lists into the same storyboard table. Sometimes the list should show indicators, other times no accessory is necessary. To do this, I added a button in the storyboard, centered vertically and trailing = 0 to the right container view. Then I gave it a width of 0 and gave that constraint an IBOutlet in my code. When I want the accessory, I simply give it a width. To make it disappear, I set its width back to 0.
Accessories and Core Data
Accessories are kind of a pain because if the user checks something off then closes the app, they expect it to be remembered. So for every change, you need to save that cells state in CoreData. I added an attribute called "state" and give it a value of "selected" when the accessory should show up filled.
This also means that I have to sort by that attribute when retrieving the list. If you already have a sort descriptor, you'll now need several.