How to programmatically select a row in UITableView in Swift - ios

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:)

Related

iOS TableView ScrollToRow Empty Screen Problem

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

Setting UITableView row selection programmatically returns nil

Trying to call a TableView row to be selected programatically.
All my delegates are set properly, and the UITableView is linked in IB. All other UITableView methods work properly, and are located in either the main ViewController or its extension. When the following is called I get Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value.
func selectTableViewRowProgrammatically () {
let indexPath = IndexPath(row: someVariable, section: 0)
tableView.selectRow(at: indexPath, animated: false, scrollPosition: UITableView.ScrollPosition.none)
tableView.delegate?.tableView!(self.tableView, didSelectRowAt: indexPath)
tableView.reloadData()
}
Error shows on self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: UITableView.ScrollPosition.none)
Thanks!
can you try Following code
let indexPath = IndexPath(row: someVariable, section: 0)
UIView.animate(withDuration: 0.1, animations: {
tableView.scrollToRow(at: indexPath, at: .top, animated: false)
}) { (true) in
tableView.delegate?.tableView!(self.tableView, didSelectRowAt: indexPath)
}

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)
}
}
}
}

How to select UITableView cell programmatically (Swift 2)

I have an UITableViewController and I need to select and scroll to one of the cells when the view loads. The reason I need the cell selected rather than just making it look selected (with an accessory or whatever) is that I want it to have a different background and no separators above/below (which is what happens when a table cell is selected), and I want the cell initially visible (if it's too far down).
I've read the two other answers and they don't work or I don't understand them. I'm not sure where I should put the self.tableView.selectRowAtIndexPath and self.scrollToRowAtIndexPath. I tried putting it in viewDidLoad() and it had no effect.
Try to use viewDidAppear
func viewDidAppear(animated: Bool){
super.viewDidAppear(animated)
let path = NSIndexPath(forRow: 1, inSection: 0)
tableView.selectRowAtIndexPath(myPath, animated: false, scrollPosition: UITableViewScrollPosition.None)
}
Swift 4:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let path = IndexPath(row: 0, section: 0)
tableView.selectRow(at: path, animated: false, scrollPosition: .none)
}
Use the selectRowAtIndexPath method of UITableView in your view controller's viewDidAppear method
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// replace forRow: value with the index of the cell
// replace inSection: value with the section the cell is in
let indexPath = NSIndexPath(forRow: 2, inSection: 0)
tableView.selectRowAtIndexPath(indexPath, animated: true, scrollPosition: .Middle)
}
Given solutions didn't work for me. So, here is the solution for swift 5
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
//Choose the row and section
let indexPath = IndexPath(row: 0, section: 0)
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle)
tableView.delegate?.tableView!(tableView, didSelectRowAt: indexPath)
}

UITableView didDeselectRowAtIndexPath delegate method not called after programmatically selecting row

I have a custom class (superclass UITableViewController).
I have an add button that:
Begins table updates.
Inserts row in table.
Ends table updates.
Calls selectRowAtIndexPath.
Calls didSelectRowAtIndexPath.
Unfortunately, didDeselectRowAtIndexPath is not being called after programatically selecting a row.
Note: Using Swift (not Objective-C).
#IBAction func addButton(sender: UIBarButtonItem) {
let indexPathZero: NSIndexPath = NSIndexPath(forRow: 0, inSection: 0)
todoController.addNewTodoItem(nameOfItem: "test5")
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths([indexPathZero], withRowAnimation: UITableViewRowAnimation.Top)
self.tableView.contentOffset.y = 64
self.tableView.endUpdates()
// Ask user for input
editCell(indexPathZero)
}
func editCell(indexPath: NSIndexPath) {
self.tableView.selectRowAtIndexPath(indexPath, animated: true, scrollPosition: UITableViewScrollPosition.None)
self.tableView(self.tableView, didSelectRowAtIndexPath: indexPath)
}
Solution
let indexPathZero: NSIndexPath = NSIndexPath(forRow: 0, inSection: 0)
todoController.addNewTodoItem(nameOfItem: "test5")
let currSelectedIndexPath = self.tableView.indexPathForSelectedRow()
if currSelectedIndexPath != nil {
self.tableView.deselectRowAtIndexPath(currSelectedIndexPath!, animated: true)
self.tableView(self.tableView, didDeselectRowAtIndexPath: currSelectedIndexPath!)
}
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths([indexPathZero], withRowAnimation: UITableViewRowAnimation.Top)
self.tableView.contentOffset.y = 64
self.tableView.endUpdates()
// Ask user for input
editCell(indexPathZero)
See the apple docs. When you call selectRowAtIndexPath, it will not call didSelectRowAtIndexPath.
"Calling this method does not cause the delegate to receive a tableView:willSelectRowAtIndexPath: or tableView:didSelectRowAtIndexPath: message, nor does it send UITableViewSelectionDidChangeNotification notifications to observers."

Resources