iOS TableView ScrollToRow Empty Screen Problem - ios

i am trying to use scrollToRow but when call this method tableview does not show any data sometimes. When i check UI inspector i can see table cells but does not seen on screen.
I tried to DispactQueue didn't solve this problem
var forIndex = 0
for item in filteredData {
if let firstCharacter = item.Name?.prefix(1), firstCharacter == char {
let indexpath = IndexPath(row: forIndex, section: 0)
self.tableView.reloadRows(at: [indexpath], with: UITableView.RowAnimation.top)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500), execute: { [weak self] in
self?.tableView.scrollToRow(at: indexpath, at: UITableView.ScrollPosition.top, animated: true)
})
break
}
forIndex += 1
}
char is element of list like a,b,c,d... FilteredData is list of tableview elements
When debugging if scrollToRow method marked breakpoint it's working

Probably the reloadRows (at:) animation is still in progress when you kick of the next causing this strange behavior.
With the breakpoint, the reloadRows (at:) has a chance to finish and on continuing the scrollToRow kicks off after.
Try one of the following if this helps your situation assuming there is no issue with your data source:
1. Change
tableView.reloadRows(at: [indexpath], with: .top)
to
tableView.reloadRows(at: [indexpath], with: .none) so that the UITableView applies a default animation
OR
2. Increase your delay before kicking off the next animation
OR
3. Perform the reload without animation
UIView.performWithoutAnimation { [weak self] in
self?.tableView.reloadRows(at: [indexpath], with: .none)
}
tableView.scrollToRow(at: indexpath,
at: UITableView.ScrollPosition.top,
animated: true)
OR
4. Use beginUpdates and endUpdates which can be useful when performing batch operations on table views
tableView.beginUpdates()
tableView.reloadRows(at: [indexpath], with: .top)
tableView.scrollToRow(at: indexpath,
at: UITableView.ScrollPosition.top,
animated: true)
tableView.endUpdates()
Do any of these give you better results ?
Update
Looking more at the docs for reloadRows(at indexPaths:, here are some lines that stood out for me
Call this method if you want to alert the user that the value of a
cell is changing. If, however, notifying the user is not
important—that is, you just want to change the value that a cell is
displaying—you can get the cell for a particular row and set its new
value.
So it seems that in some situations animation might not be needed as the cell could be off screen or not needed, so the simplest way to change data is get the cell at the index path and change the data without this function.
Since you are running this in a loop, you most likely start the next indexPath's reloadRows before the previous indexPath's scrollToRow is complete and this can cause some unusual behavior.
Since UITableView does not have it's own completion handler, we can try using CoreAnimation with recursion which can be option 5.
Here is small example I prepared. My sample is like this:
I have 15 rows that are grey in color
I decide that I want to change the color of 4 rows
I store the index of 4 rows in a queue
I will change the color using tableView.reloadRows(at:
After changing the color, I want to scroll to that row using tableView.scrollToRow(at:
Here is how I accomplish that
First I extend the UITableView to use CoreAnimation block to do reloadRows and scrollToRow
extension UITableView
{
func reloadRows(at indexPaths: [IndexPath],
with animation: UITableView.RowAnimation,
completion: (() -> Void)?)
{
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
reloadRows(at: indexPaths, with: animation)
CATransaction.commit()
}
func scrollToRow(at indexPath: IndexPath,
at scrollPosition: UITableView.ScrollPosition,
animated: Bool,
completion: (() -> Void)?)
{
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
CATransaction.setAnimationDuration(2) // set what you want
scrollToRow(at: indexPath, at: scrollPosition, animated: animated)
CATransaction.commit()
}
}
Here is how I use this extension with my view controller set up
class TableViewAnimation: UITableViewController
{
let numberOfRows = 15
// Change the color of these rows in tableview
var colorChangeArray: [Int] = []
// Copy of colorChangeArray used in recursion
var colorChangeQueue: [Int] = []
// Change the color of row to this
private let colorToChange = UIColor.systemBlue
// Normal cell color
private let colorNormal = UIColor.gray
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .white
setUpNavigationBar()
setUpTableView()
}
private func setUpNavigationBar()
{
title = "Table View Animate"
let barButton = UIBarButtonItem(title: "Animate",
style: .plain,
target: self,
action: #selector(didTapAnimateButton))
navigationItem.rightBarButtonItem = barButton
}
private func setUpTableView()
{
tableView.register(CustomCell.self,
forCellReuseIdentifier: CustomCell.identifier)
}
#objc
func didTapAnimateButton()
{
// Queue all the rows that should change
// We will dequeue these in the animation function
// and the recursive function stops when the queue
// is empty
colorChangeQueue = [1, 3, 6, 12]
resumeAnimation()
}
// Recursion function rather than loops using our
// custom functions from extension
private func resumeAnimation()
{
if !colorChangeQueue.isEmpty
{
let rowToChange = colorChangeQueue.removeFirst()
print("starting \(rowToChange) animation")
let indexPath = IndexPath(row: rowToChange,
section: 0)
colorChangeArray.append(rowToChange)
tableView.reloadRows(at: [indexPath],
with: .top) { [weak self] in
self?.tableView.scrollToRow(at: indexPath,
at: .top,
animated: true,
completion: {
// recursively call the function again with a small delay
DispatchQueue.main.asyncAfter(deadline: .now() + 1)
{
print("complete \(rowToChange) animation")
self?.resumeAnimation()
}
})
}
}
}
}
Finally, here is the data source and delegate but nothing unique is happening here, just adding it for completeness
extension TableViewAnimation
{
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
return numberOfRows
}
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell
= tableView.dequeueReusableCell(withIdentifier: CustomCell.identifier) as! CustomCell
if colorChangeArray.contains(indexPath.row)
{
cell.mainView.backgroundColor = colorToChange
}
else
{
cell.mainView.backgroundColor = colorNormal
}
cell.textLabel?.textAlignment = .center
cell.textLabel?.text = "Row \(indexPath.row)"
return cell
}
override func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat
{
return 150
}
}
Now it seems like the animations of each of the row is sequenced properly:
And also if you see the print in the console, it is sequenced which did not happen with the loop method:
starting 1 animation
complete 1 animation
starting 3 animation
complete 3 animation
starting 6 animation
complete 6 animation
starting 12 animation
complete 12 animation

Related

UITableView resets to start position before the user has released it

I have an app with an UITableView and its data gets updated regularly. If the data receives new element, the table view is reloaded. Let’s say the table view has place for 10 visible cells, but data for only 2 of them. The user has scrolled in either direction and not released the table view from touch. If the user has scrolled up, they may have hidden the first cell and only the second one would be visible. Then a new element is received and reloadData is called. Instead of waiting for releasing the table view to update, the tableview gets updated right away and the contentOffset is reset to 0. The tableView just resets to start position while the user has scrolled and not released.
I tried similar setup in separate Xcode project and the issue does not appear there. I wonder what the difference could be.
This is some of the code:
For the ViewController that is the dataSource:
func viewDidLoad() {
super.viewDidLoad()
//some other code
DataManager.shared.onElementReceival = { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return DataManager.shared.data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
cell.textLabel?.textColor = .white
cell.backgroundColor = .clear
if let name = DataManager.shared.data[indexPath.row].name {
cell.textLabel?.text = name
} else {
cell.textLabel?.text = "Unnamed"
}
return cell
}
From the DataManager
func didReceive(_ element: Element) {
data.append(element)
onElementReceival()
}
Try to reload only cell using:
tableview.insertRowsAtIndexPaths([IndexPath(forRow: Yourarray.count-1, inSection: 0)], withRowAnimation: .Automatic)
In your example:
let onElementReceival:(IndexPath) -> Void = { [weak self] inx in
guard let self = self else { return }
DispatchQueue.main.async {
tablview.beginUpdates()
tablview.insertRowsAtIndexPaths(at: [inx], with: .automatic)
tablview.endUpdates()
}
}
From DataManager
func didReceive(_ element: Element) {
data.append(element)
onElementReceival(IndexPath(row:data.count, section: 0))
}

How I can reload tableview data after the animation of fade swipeAction completed

I use SwipeCellKit for do swipe actions for my tableview.
I try to do the left swipe for check or unckech for accessoryType of my cell and everything work fine, but after i press check the tableview reload the data immediatelly and i can't see the animation of roollback the check button. So i want to ask how i can call reload data after this animation end.
I have something like this:
I have something like this:
But i want fade animation like this
I want this animation
My code:
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? {
var action = super.tableView(tableView, editActionsForRowAt: indexPath, for: orientation) ?? []
guard orientation == .left else { return action }
let checkAction = SwipeAction(style: .destructive, title: nil) { action, indexPath in
self.completeItem(at: indexPath)
}
action.append(checkAction)
return action
}
private func completeItem(at indexPath: IndexPath){
if let item = itemArray?[indexPath.row]{
do{
try realm.write {
item.done = !item.done
}
}
catch{
print( error.localizedDescription)
}
}
tableView.reloadData()
}
In order to hide the action on select, you need to set the hidesWhenSelected property for the action.
checkAction.hidesWhenSelected = true
Also, the example doesn't reload the tableView. Instead, they use an animation to remove the dot. You will need to manually delay your reload until the hide animation is complete.
See line 156 for the property
See lines 256 - 273 for the animation
https://github.com/SwipeCellKit/SwipeCellKit/blob/develop/Example/MailExample/MailTableViewController.swift

ScrollToRow does not work when said row is near the bottom of the table view

I have a cell class which implements a textfield delegate. In this delegate I am calling a function to tell the tableview to scroll to a specific row based off an indexPath. This works in most cases but not when the row is at the bottom of the table view. The cell class has a table property which is passed in, in my main controllers cellForRow method. Code below:
extension IR_TextCell: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
util_textField.addHightlightedBorder(textField)
if let index = table?.indexPath(for: self)
{
scrollDelegate?.scrollToMe(at: index)
}
}
}
func scrollToMe(at index: IndexPath) {
self.tableV.scrollToRow(at: index, at: .middle, animated: false)
}
I have tried wrapping DispatchQueue.main.async around this and adding a deadline but it didn't make a difference.
Do I need to change my tableview's bottom constraint maybe?
My situation is a little different than yours but I had a same issue scrolling to cells that are near the bottom. It might now work for your exact situation but I hope this helps someone who comes across this posting. I suspected that it might be a timing issue so I ended up doing it like below:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == tableView.indexPathsForVisibleRows?.last?.row {
let scrollIndex = 0//set to your predetermined scrolled to index
let cellRect = tableView.rectForRow(at: indexPath)
let completelyVisible = tableView.bounds.contains(cellRect)
if scrollIndex >= indexPath.row && !completelyVisible {
let maxIndex = 10//number of elements in the array - 1
//in case you want a delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0) {
self.tableView.scrollToRow(at: IndexPath(row: scrollIndex > maxIndex ? maxIndex : scrollIndex, section: 0), at: .middle, animated: false)
}
}
}
}

Cells temporarily disappearing when TableView beginUpdates / endUpdates

I am trying to make a expandable table view (static cells). A container view is placed inside a cell below the "Lists of options" cell, as shown below:
The problem comes with the animation when expanding / collapsing. The topmost cell of each section will disappear briefly(hidden?), then reappear after the animation ends. I have later experimented and the results are as followed:
This will not happen when using tableView.reloadData().
This will happen when using tableView.beginUpdates() / endUpdates() pairs.
Hence I have come to the conclusion that this has sth to do with the animation. Below is my code and pictures for further explanation for said issue.
Code for hiding / showing
private func hideContainerView(targetView: UIView) {
if targetView == violationOptionsContainerView {
violationOptionContainerViewVisible = false
}
tableView.beginUpdates()
tableView.endUpdates()
UIView.animate(withDuration: 0.1, animations: { targetView.alpha = 0.0 }) { _ in
targetView.isHidden = true
}
}
private func showContainerView(targetView: UIView) {
if targetView == violationOptionsContainerView {
violationOptionContainerViewVisible = true
}
tableView.beginUpdates()
tableView.endUpdates()
targetView.alpha = 0.0
UIView.animate(withDuration: 0.1, animations: { targetView.alpha = 1.0 }) { _ in
targetView.isHidden = false
}
}
Table view
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
switch cell {
case violationTableViewCell:
if violationOptionContainerViewVisible {
hideContainerView(targetView: violationOptionsContainerView)
} else {
showContainerView(targetView: violationOptionsContainerView)
}
default: break
}
// tableView.reloadData()
// if we use this instead of begin/end updates, the cells won't disappear. But then we'll be ditching animation too.
}
tableView.deselectRow(at: indexPath, animated: true)
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 0 && indexPath.row == 3 {
if !violationOptionContainerViewVisible {
return 0.0
}
}
return super.tableView(tableView, heightForRowAt: indexPath)
}
Images
Note that the first cell in each section ("FFFF" and the "slider bar") disappears and reappears during the animation
After searching for a few days, I have found the cause to this behavior.
I have, for some reason, set the cells layer.zposition below default zero. This will cause the cell to be seen "below" it's background view (hence disappeared) during the animation even though it's a subview of it.
Adjusting the value back to 0 or higher will remove this issue.

How to programmatically select a row in UITableView in Swift

I need to select a row in a UITableView programmatically using Swift 1.2.
This is the simple code:
var index = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.selectRowAtIndexPath(index, animated: true, scrollPosition: UITableViewScrollPosition.Middle)
self.tableView(self.tableView, didSelectRowAtIndexPath: index)
The above gives me the following error:
Cannot invoke 'selectRowAtIndexPath' with an argument list of type '(NSIndexPath!, animated: Bool, scrollPosition: UITableViewScrollPosition)'
What is wrong with my Swift 1.2 code?
My UItableView has been created in IB in the UIViewController that I am trying to call the code above.
When I put the code in a UITableViewController the compiler does not give any errors.
Do I need to embed a UITableViewController in a container or is there another way?
Swift 3 to Swift 5 Solution
Selecting a Row
let indexPath = IndexPath(row: 0, section: 0)
myTableView.selectRow(at: indexPath, animated: true, scrollPosition: .bottom)
myTableView.delegate?.tableView!(myTableView, didSelectRowAt: indexPath)
DeSelecting a Row
let deselectIndexPath = IndexPath(row: 7, section: 0)
myTableView.deselectRow(at: deselectIndexPath, animated: true)
myTableView.delegate?.tableView!(myTableView, didDeselectRowAt: indexPath)
The statement
self.tableView.selectRowAtIndexPath(index, animated: true, scrollPosition: UITableViewScrollPosition.Middle)
assumes that tableView is a property of the view controller, connected
to a table view in the Storyboard. A UITableViewController, for example, already has this
property.
In your case, the view controller is a not a table view controller
but a subclass of a UIViewController. It also has an outlet that is
connected to the table view, but it is not called
tableView but menuTable. Then of course you have to call
self.menuTable.selectRowAtIndexPath(index, animated: true, scrollPosition: UITableViewScrollPosition.Middle)
to select a row of that table view.
The strange error messages are caused by the fact that
self.tableView can also be understood by the compiler as a
"curried function" (compare http://oleb.net/blog/2014/07/swift-instance-methods-curried-functions/).
Use below code,after loading your table view with data:
let rowToSelect:NSIndexPath = NSIndexPath(forRow: 0, inSection: 0); //slecting 0th row with 0th section
self.tableView.selectRowAtIndexPath(rowToSelect, animated: true, scrollPosition: UITableViewScrollPosition.None);
now,you have to manually call didSelectRowAtIndexPath: delegate method using below code:
self.tableView(self.tableView, didSelectRowAtIndexPath: rowToSelect); //Manually trigger the row to select
Thanks.
Swift 3.x
if you want to do it at the 'cell-creation', you can do it like this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = TableViewCell()
let item = items[indexPath.row]
cell.textLabel?.text = item.title
if (item.checked) {
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
}
return cell
}
Using Swift 2.x, as described by Pankaj purohit answers the correct method is:
func tapRowAtIndex(index:Int) {
let rowToSelect:NSIndexPath = NSIndexPath(forRow: index, inSection: 0)
self.tableView.selectRowAtIndexPath(rowToSelect, animated: true, scrollPosition: UITableViewScrollPosition.None)
self.tableView(self.tableView, didSelectRowAtIndexPath: rowToSelect)
}
Keep in mind that if you call this method from an external class for example, you dont know when tableView has finished its loading, so what's the possibilities?, how to workaround this problem? :
Step one: create a class boolean var
var automatingTap: Bool = false
Step two: check when the table finish its loading and launch an "end operations" method:
func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath)
{
let lastRowIndex = tableView.numberOfRowsInSection(0)
if indexPath.row == lastRowIndex - 1 {
endOperations()
}
}
func endOperations()
{
print("finished loading")
if automatingTap {
tapRowAtIndex(0)
automatingTap = false
}
}
Step three: call my tableView class from another class
for example:
override func prepareForSegue(segue: UIStoryboardSegue?, sender: AnyObject?) {
if segue!.identifier == "DetailsTableView" {
let viewController:ViewController = segue!.destinationViewController as ViewController
viewController.automatingTap = true
}
}
Reusable function with validation of table size
Swift 4 and 5
This reusable function works and validate the size of table.
func selectRow(tableView: UITableView, position: Int) {
let sizeTable = tableView.numberOfRows(inSection: 0)
guard position >= 0 && position < sizeTable else { return }
let indexPath = IndexPath(row: position, section: 0)
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle)
}
you can use it in this way
selectRow(tableView: myTableView, position: pos)
or you can implement this extension:
extension UITableView {
func selectRow(row: Int, section: Int = 0) {
let sizeTable = self.numberOfRows(inSection: section)
guard row >= 0 && row < sizeTable else { return }
let indexPath = IndexPath(row: row, section: section)
self.selectRow(at: indexPath, animated: true, scrollPosition: .middle)
}
}
and you can use it in this way:
mytableView.selectRow(row: pos, section: 0)
or
mytableView.selectRow(row: pos)
Swift 4.2:
Select one or more Rows
let ndx:IndexSet = [1]
// or: let ndx:IndexSet = [1, 2, 3]; // etc
tableview?.selectRowIndexes(ndx, byExtendingSelection: false);
Deselect a Single Row
tableview?.deselectRow(current)
Note that if you have (func tableViewSelectionDidChange(...)) implemented, that will be triggered by the above.
Also see Charlton Provatas' answer at https://stackoverflow.com/a/48696458/352920 for an extension to NSTableView, that provides a simple
tableview?.selectRow(at:)

Resources