tableView:didEndEditingRowAtIndexPath: delegate method called twice - ios

I would like to know when the user applies swipe actions on cells of a UITableView. As per the doc, the UITableViewDelegate methods should I use are the following:
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath;
The willBegin... is called once while the didEnd... is called twice. Is there any reason for this?
My goal is to know when the user has performed a swipe gesture on a cell, followed by a cancel one (he doesn't want to delete anything). This in order to restore the previous selected cell if no action was performed (as per UITableView loses selection).
Any hints?

My solution is described in my blog Restore the selection of a UITableViewCell after cancelling the “Swipe to delete” operation (Dec 22nd, 2014). To summarize, use a boolean value that keeps track of operations.
I opened a radar. I will wait for a reply and I will update with the feedback.
func tableView(tableView: UITableView, willBeginEditingRowAtIndexPath indexPath: NSIndexPath) {
self.swipeGestureStarted = true
}
func tableView(tableView: UITableView, didEndEditingRowAtIndexPath indexPath: NSIndexPath) {
if(self.swipeGestureStarted) {
self.swipeGestureStarted = false
self.tableView.selectRowAtIndexPath(self.selectedIndexPath, animated: true, scrollPosition: .None)
}
}

I encountered this problem too, and was able to solve it by declaring a BOOL as a member to my view controller:
#interface ViewController ()
#property (nonatomic, assign) BOOL isEditingRow;
#end
#implementation ViewController
...
... and then setting and reading the value of the BOOL in the UITableView's delegate methods:
-(void)tableView: (UITableView*)tableView willBeginEditingRowAtIndexPath:(NSIndexPath*)indexPath
{
self.isEditingRow = YES;
}
-(void)tableView: (UITableView*)tableView didEndEditingRowAtIndexPath:(NSIndexPath*)indexPath
{
if (self.isEditingRow)
{
self.isEditingRow = NO;
// now do processing that you want to do once - not twice!
}
}
It's more of a workaround, but very frustrating that this occurs at all.

Related

How to manage UIbutton in UItableViewCell without using sender.tag method? [duplicate]

This question already has answers here:
iOS get specific UITableViewCell
(5 answers)
Correct way to setting a tag to all cells in TableView
(3 answers)
Closed 5 years ago.
I have a cell with a button inside. In my cellForRowAtIndexPath method i refeer to my uibutton in this way:
UIButton *Button= (UIButton *) [cell viewWithTag:3];
I have found a solution, something like this:
Button.tag = indexPath.row;
[Button addTarget:self action:#selector(yourButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
-(void)yourButtonClicked:(UIButton*)sender
{
if (sender.tag == 0)
{
// Your code here
}
}
But my button.tag is already used to identify the UIbutton view from the others views inside the cell, i can't use it for the indexpath.row. How i can do in my case?
EDIT
Finally i have made a Category for the UIButton, it seems the faster ad reusable solutions but every answers here is a valid option
Create custom button subclass of UIButton and define property based on your need. Button.tag is a default identifier. In custom class you can give as many property as you want. Then user this Custom button class instead of UIButton. And user custom properties followed by dot(.) as like as tag.
You need not access the button using tag and then set Selectorfor each cell. I believe you can achieve much cleaner approach
Step 1:
In your Custom cell, drag the IBAction from button in cell to your custom cell.
#IBAction func buttonTapped(_ sender: Any) {
//wait for implementation
}
Step 2:
In your custom cell, now declare a protocol
protocol CellsProtocol : NSObjectProtocol {
func buttonTapped(at index : IndexPath)
}
and while in being same class, create few variables as well.
weak var delegate : CellsProtocol? = nil
var indexPath : IndexPath! = nil
we will see the usage of these variables soon :)
Step 3:
Lets get back to IBAction we dragged just now
#IBAction func buttonTapped(_ sender: Any) {
self.delegate?.buttonTapped(at: self.indexPath)
}
Step 4:
Now you can get back to your UITableViewController and confirm to the protocol you declared just now
extension ViewController : CellsProtocol {
func buttonTapped(at index: IndexPath) {
//here you have indexpath of cell whose button tapped
}
}
Step 5:
Now finally update your cellForRowAtIndexPath as
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : MyTableViewCell = ;
cell.indexPath = indexPath
cell.delegate = self
}
}
Thats it :) Now you have a button action which tells you, button in which cell tapped :) Hope it helps :) This is more generic approach, because even if you have multiple uicomponents you can still use this approach.
EDIT 1:
In comments below rmaddy pointed out that having a indexPath as a property cell might lead to issues and cell should not care to know its indexPath and protocol should be modified to return the cell rather than returning the index path.
Quoting comment :
Not necessarily. You are assuming reloadData is being called. You can
have a visible cell and then insert a row above it. That visible cell
is not updated and its indexPath is not different but the cell's
indexPath property that you have is not updated. Use the cell itself
as the argument. It's much better than passing the index path. If the
index path is needed by the delegate, the delegate can ask the table
view what the cell's current index path is. A cell should never care
or know what its index path is.
The statement above makes sense especially the fact that A cell should never care or know what its index path is. Hence updating my answer below
Step 1:
Go ahead and delete the indexPath property in cell :)
Step 2:
Modify protocol to
protocol CellsProtocol : NSObjectProtocol {
func buttonTapped(in cell : UITableViewCell)
}
Step 3:
Modify your IBAction as
#IBAction func buttonTapped(_ sender: Any) {
self.delegate?.buttonTapped(in: self)
}
Step 4:
Finally modify your protocol confirmation in your ViewController to
extension ViewController : CellsProtocol {
func buttonTapped(in cell: UITableViewCell) {
let indexPath = self.tableView.indexPath(for: cell)
//here you have indexpath of cell whose button tapped
}
}
Thats it :)
Another option - use a "call back" block.
This assumes you have a Prototype cell with "btnCell" identifier, containing a UIButton connected to the - (IBAction)buttonTapped:(id)sender method shown below...
Your cell class:
//
// WithButtonTableViewCell.h
//
// Created by Don Mag on 11/15/17.
//
#import <UIKit/UIKit.h>
#interface WithButtonTableViewCell : UITableViewCell
- (void)setButtonTappedBlock:(void (^)(id sender))buttonTappedBlock;
#end
//
// WithButtonTableViewCell.m
//
// Created by Don Mag on 11/15/17.
//
#import "WithButtonTableViewCell.h"
#interface WithButtonTableViewCell ()
#property (copy, nonatomic) void (^buttonTappedBlock)(id sender);
#end
#implementation WithButtonTableViewCell
- (IBAction)buttonTapped:(id)sender {
// call back if the block has been set
if (self.buttonTappedBlock) {
self.buttonTappedBlock(self);
}
}
#end
Your table view controller class:
//
// WithButtonTableViewController.h
//
// Created by Don Mag on 7/12/17.
//
#import <UIKit/UIKit.h>
#interface WithButtonTableViewController : UITableViewController
#end
//
// WithButtonTableViewController.m
//
// Created by Don Mag on 11/15/17.
//
#import "WithButtonTableViewController.h"
#import "WithButtonTableViewCell.h"
#interface WithButtonTableViewController ()
#end
#implementation WithButtonTableViewController
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 20;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
WithButtonTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"btnCell" forIndexPath:indexPath];
[cell setButtonTappedBlock:^(id sender) {
NSLog(#"Button in cell at row %ld in section: %ld was tapped", (long)indexPath.row, (long)indexPath.section);
// sender is the cell
// do whatever else you want here...
}];
return cell;
}
#end
Now, when the button in the cell is tapped, it will "call back" to the view controller, at which point the code inside the Block will be executed.

How to prevent clicking UITableView second time

My app calls a block in tableView:didSelectRowAtIndexPath and in the block it presents a view controller. If I click the cell second time when the first click is in progress, it crashes.
How can I prevent the cell to be clicked second time?
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
[dataController fetchAlbum:item
success:^(Album *album) {
...
...
[self presentViewController:photoViewController animated:YES completion:nil];
}];
At the beginning of didSelectRow, turn off user interaction on your table.
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
tableView.userInteractionEnabled = NO;
...
You may want to turn it back on later in the completion of fetchAlbum (Do this on the main thread) so that if the user comes back to this view (or the fetch fails), they can interact with the table again.
For swift 3 :
When user select a row, turn off user interactions :
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.isUserInteractionEnabled = false
Don't forget to turn it on back whenever the view appear :
override func viewDidAppear(_ animated: Bool) {
tableView.isUserInteractionEnabled = true
}
You could either prevent multiple clicks (by disabling the table or covering it up with a spinner) or you could make didSelectRowAtIndexPath present your view controller synchronously and load your "album" after it's been presented. I'm a fan of the latter as it makes the UI feel more responsive.
For a cleaner approach:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
defer {
tableView.isUserInteractionEnabled = true
}
tableView.isUserInteractionEnabled = false
}
This way you're much less prone to errors due to forgetfulness.
You want to disable user interaction on the cell:
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
cell.userInteractionEnabled = NO;
As Stonz2 points out, you probably want to do it to the entire tableview though, rather than the specific cell if you're presenting a VC.
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell?.userInteractionEnabled = false
then
set back to true before you navigate to another view, otherwise when you return back to your table the cell will be still disabled.
I have a similar approach like Skaal answered but in a different way. This solution will work for any swift version .
Create a property named isPresentingVC in your view controller and set it to true.
var isPresentingVC: Bool = true
Inside didSelect row, try this
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if isPresentingVC {
isPresentingVC = false
//do your work like go to another view controller
}
}
Now in viewWillAppear or viewDidDisappear reset its value to true
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
isPresentingVC = true
}

Swipe to delete cell does not cancel UIButton action

My UITableView has the swipe to delete feature enabled. Each cell has a UIButton on it that performs an action (in this case, perform a segue).
I'd expect that if I swipe the cell by touching the button, the button's action would be canceled/ignored, and only the swipe would be handled. What actually happens, however, is that both gestures (swipe + tap) are detected and handled.
This means that if I just want to delete one cell and "accidentally" swipe by touching the button, the app will go to the next screen.
How can I force my app to ignore the taps in this case?
august's answer was nice enough for me, but I figured out how to make it even better:
Checking if the table was on edit mode to decide if the button should perform its action will make it behave as it should, but there will still be an issue in the user experience:
If the user wants to exit the editing mode, he should be able to tap anywhere in the cell to achieve that, including the button. However, the UIButton's action is still analyzed first by the app, and tapping the button will not exit editing mode.
The solution I found was to disable the button's user interaction while entering edit mode, and reenabling it when it's done:
// View with tag = 1 is the UIButton in question
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath {
[(UIButton *)[[tableView cellForRowAtIndexPath:indexPath] viewWithTag:1] setUserInteractionEnabled:NO];
}
- (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath {
[(UIButton *)[[tableView cellForRowAtIndexPath:indexPath] viewWithTag:1] setUserInteractionEnabled:YES];
}
This way, dragging the button to enter edit mode will not trigger the button's action, and taping it to exit edit mode will indeed exit edit mode.
One elegant way would be to ignore button taps as long as a cell has entered editing mode. This works because the swipe to delete gesture will cause willBeginEditingRowAtIndexPath to be called before the button tap action is invoked.
- (void)tableView:(UITableView*)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
self.isEditing = YES;
}
- (void)tableView:(UITableView*)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
self.isEditing = NO;
}
// button tapped
- (IBAction)tap:(id)sender
{
if (self.isEditing) {
NSLog(#"Ignore it");
}
else {
NSLog(#"Tap");
// perform segue
}
}
It's possible to do it also in your cell's subclass:
override func setEditing(editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
actionButton?.userInteractionEnabled = !editing
}
Because my function being called from a button press was a delegate onto my main UITableViewController's class and connected to the UITableViewCell as an IBAction, the button in my UITableViewCell was still firing on a swipe that had the UIButton pressed as part of the swipe.
In order to stop that I used the same UITableView delegates as the accepted answer, but had to set a file level variable to monitor if editing was occurring.
// in custom UITableViewCell class
#IBAction func displayOptions(_ sender: Any) {
delegate?.buttonPress()
}
// in UITableViewController class that implemented delegate
fileprivate var cellSwiped: Bool = false
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
cellSwiped = true
}
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) {
cellSwiped = false
}
func displayContactsSheet(for contact: Contact) {
if cellSwiped {
return
}
// proceed with delegate call button press actions
}

didSelectViewController not getting called when in "More" section

I have a UITabBarController and I have set up its delegate method didSelectViewController, as I am interested in the index of the tab that is being selected.
However, I noticed that the didSelectViewController method doesn't get called when the user is in the "More" section (when there are more tabs than can be shown in the tabbar):
Is there a way for me to get notified of the items the user selects from the table that is being automatically created?
I found what I needed in this question.
Basically you set up a UITabBarControllerDelegate and a UINavigationControllerDelegate for the navigation controller that is displayed inside the More tab. After that you detect if the user touched one of the visible tabs, or the "More" tab.
EDIT
Also, to directly manipulate the table that is visible within the "More" navigation controller, you can set up a "man-in-the-middle" table view delegate, that intercepts the calls to the original delegate. See code from inside didSelectViewController below:
if (viewController == tabBarController.moreNavigationController && tabBarController.moreNavigationController.delegate == nil) {
// here we replace the "More" tab table delegate with our own implementation
// this allows us to replace viewControllers seamlessly
UITableView *view = (UITableView *)self.tabBarController.moreNavigationController.topViewController.view;
self.originalDelegate = view.delegate;
view.delegate = self;
}
After that, you are free to do whatever you like inside the delegate methods, as long as you call the same methods in the other delegate (I actually checked to which methods the original delegate responds, and the only delegate method that is implemented is the didSelectRow:forIndexPath:). See an example below:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// this is the delegate for the "More" tab table
// it intercepts any touches and replaces the selected view controller if needed
// then, it calls the original delegate to preserve the behavior of the "More" tab
// do whatever here
// and call the original delegate afterwards
[self.originalDelegate tableView: tableView didSelectRowAtIndexPath: indexPath];
}
Previous answer is almost correct because it misses one method to work properly.
class MyClass: ... {
var originalTableDelegate: UITableViewDelegate?
}
extension MyClass: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if viewController == tabBarController.moreNavigationController && originalTableDelegate == nil {
if let moreTableView = tabBarController.moreNavigationController.topViewController?.view as? UITableView {
originalTableDelegate = moreTableView.delegate
moreTableView.delegate = self
}
}
}
}
extension MyClass: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
originalTableDelegate!.tableView!(tableView, willDisplay: cell, forRowAt: indexPath)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("intercepted")
originalTableDelegate?.tableView!(tableView, didSelectRowAt: indexPath)
}
}
The original table delegate on more controller is actually system hidden class UIMoreListController. If we take a look into its implementation we will notice these two overrided functions: didSelect and willDisplay.
NOTE:
There could be a potential problem with this delegate interception if Apple decide to implement some other delegate method in its own UIMoreListController in future iOS versions.

How to detect the end of loading of UITableView

I want to change the offset of the table when the load is finished and that offset depends on the number of cells loaded on the table.
Is it anyway on the SDK to know when a uitableview loading has finished? I see nothing neither on delegate nor on data source protocols.
I can't use the count of the data sources because of the loading of the visible cells only.
Improve to #RichX answer:
lastRow can be both [tableView numberOfRowsInSection: 0] - 1 or ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row.
So the code will be:
-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if([indexPath row] == ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row){
//end of loading
//for example [activityIndicator stopAnimating];
}
}
UPDATE:
Well, #htafoya's comment is right. If you want this code to detect end of loading all data from source, it wouldn't, but that's not the original question. This code is for detecting when all cells that are meant to be visible are displayed. willDisplayCell: used here for smoother UI (single cell usually displays fast after willDisplay: call). You could also try it with tableView:didEndDisplayingCell:.
Swift 3 & 4 & 5 version:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let lastVisibleIndexPath = tableView.indexPathsForVisibleRows?.last {
if indexPath == lastVisibleIndexPath {
// do here...
}
}
}
I always use this very simple solution:
-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if([indexPath row] == lastRow){
//end of loading
//for example [activityIndicator stopAnimating];
}
}
Here's another option that seems to work for me. In the viewForFooter delegate method check if it's the final section and add your code there. This approach came to mind after realizing that willDisplayCell doesn't account for footers if you have them.
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
// Perform some final layout updates
if (section == ([tableView numberOfSections] - 1)) {
[self tableViewWillFinishLoading:tableView];
}
// Return nil, or whatever view you were going to return for the footer
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
// Return 0, or the height for your footer view
return 0.0;
}
- (void)tableViewWillFinishLoading:(UITableView *)tableView
{
NSLog(#"finished loading");
}
I find this approach works best if you are looking to find the end loading for the entire UITableView, and not simply the visible cells. Depending on your needs you may only want the visible cells, in which case folex's answer is a good route.
Using private API:
#objc func tableViewDidFinishReload(_ tableView: UITableView) {
print(#function)
cellsAreLoaded = true
}
Using public API:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// cancel the perform request if there is another section
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(tableViewDidLoadRows:) object:tableView];
// create a perform request to call the didLoadRows method on the next event loop.
[self performSelector:#selector(tableViewDidLoadRows:) withObject:tableView afterDelay:0];
return [self.myDataSource numberOfRowsInSection:section];
}
// called after the rows in the last section is loaded
-(void)tableViewDidLoadRows:(UITableView*)tableView{
self.cellsAreLoaded = YES;
}
A possible better design is to add the visible cells to a set, then when you need to check if the table is loaded you can instead do a for loop around this set, e.g.
var visibleCells = Set<UITableViewCell>()
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
visibleCells.insert(cell)
}
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
visibleCells.remove(cell)
}
// example property you want to show on a cell that you only want to update the cell after the table is loaded. cellForRow also calls configure too for the initial state.
var count = 5 {
didSet {
for cell in visibleCells {
configureCell(cell)
}
}
}
Swift solution:
// willDisplay function
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
let lastRowIndex = tableView.numberOfRowsInSection(0)
if indexPath.row == lastRowIndex - 1 {
fetchNewDataFromServer()
}
}
// data fetcher function
func fetchNewDataFromServer() {
if(!loading && !allDataFetched) {
// call beginUpdates before multiple rows insert operation
tableView.beginUpdates()
// for loop
// insertRowsAtIndexPaths
tableView.endUpdates()
}
}
For the chosen answer version in Swift 3:
var isLoadingTableView = true
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if tableData.count > 0 && isLoadingTableView {
if let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows, let lastIndexPath = indexPathsForVisibleRows.last, lastIndexPath.row == indexPath.row {
isLoadingTableView = false
//do something after table is done loading
}
}
}
I needed the isLoadingTableView variable because I wanted to make sure the table is done loading before I make a default cell selection. If you don't include this then every time you scroll the table it will invoke your code again.
The best approach that I know is Eric's answer at: Get notified when UITableView has finished asking for data?
Update: To make it work I have to put these calls in -tableView:cellForRowAtIndexPath:
[tableView beginUpdates];
[tableView endUpdates];
To know when a table view finishes loading its content, we first need to have a basic understanding of how the views are put on screen.
In the life cycle of an app, there are 4 key moments :
The app receives an event (touch, timer, block dispatched etc)
The app handles the event (it modifies a constraint, starts an animation, changes background etc)
The app computes the new view hierarchy
The app renders the view hierarchy and displays it
The 2 and 3 times are totally separated. Why ? For performance reasons, we don't want to perform all the computations (done at 3) each time a modification is done.
So, I think you are facing a case like this :
tableView.reloadData()
tableView.visibleCells.count // wrong count oO
What’s wrong here?
A table view reloads its content lazily. Actually, if you call reloadData multiple times it won’t create performance issues. The table view only recomputes its content size based on its delegate implementation and waits the moment 3 to loads its cells. This time is called a layout pass.
Okay, how to get involved in the layout pass?
During the layout pass, the app computes all the frames of the view hierarchy. To get involved, you can override the dedicated methods layoutSubviews, updateLayoutConstraints etc in a UIView subclass and the equivalent methods in a view controller subclass.
That’s exactly what a table view does. It overrides layoutSubviews and based on your delegate implementation adds or removes cells. It calls cellForRow right before adding and laying out a new cell, willDisplay right after. If you called reloadData or just added the table view to the hierarchy, the tables view adds as many cells as necessary to fill its frame at this key moment.
Alright, but now, how to know when a tables view has finished reloading its content?
We can rephrase this question: how to know when a table view has finished laying out its subviews?
• The easiest way is to get into the layout of the table view :
class MyTableView: UITableView {
func layoutSubviews() {
super.layoutSubviews()
// the displayed cells are loaded
}
}
Note that this method is called many times in the life cycle of the table view. Because of the scroll and the dequeue behavior of the table view, cells are modified, removed and added often. But it works, right after the super.layoutSubviews(), cells are loaded. This solution is equivalent to wait the willDisplay event of the last index path. This event is called during the execution of layoutSubviews of the table view when a cell is added.
• Another way is to be notified when the app finishes a layout pass.
As described in the documentation, you can use an option of the UIView.animate(withDuration:completion):
tableView.reloadData()
UIView.animate(withDuration: 0) {
// layout done
}
This solution works but the screen will refresh once between the time the layout is done and the time the block is executed. This is equivalent to the DispatchMain.async solution but specified.
• Alternatively, I would prefer to force the layout of the table view
There is a dedicated method to force any view to compute immediately its subview frames layoutIfNeeded:
tableView.reloadData()
table.layoutIfNeeded()
// layout done
Be careful however, doing so will remove the lazy loading used by the system. Calling those methods repeatedly could create performance issues. Make sure that they won’t be called before the frame of the table view is computed, otherwise the table view will be loaded again and you won’t be notified.
I think there is no perfect solution. Subclassing classes could lead to trubles. A layout pass starts from the top and goes to the bottom so it’s not easy to get notified when all the layout is done. And layoutIfNeeded() could create performance issues etc.
Here is how you do it in Swift 3:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
// perform your logic here, for the first row in the table
}
// ....
}
here is how I do it in Swift 3
let threshold: CGFloat = 76.0 // threshold from bottom of tableView
internal func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;
if (!isLoadingMore) && (maximumOffset - contentOffset <= threshold) {
self.loadVideosList()
}
}
Here is what I would do.
In your base class (can be rootVC BaseVc etc),
A. Write a Protocol to send the "DidFinishReloading" callback.
#protocol ReloadComplition <NSObject>
#required
- (void)didEndReloading:(UITableView *)tableView;
#end
B. Write a generic method to reload the table view.
-(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
In the base class method implementation, call reloadData followed by delegateMethod with delay.
-(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[tableView reloadData];
if(aViewController && [aViewController respondsToSelector:#selector(didEndReloading:)]){
[aViewController performSelector:#selector(didEndReloading:) withObject:tableView afterDelay:0];
}
}];
}
Confirm to the reload completion protocol in all the view controllers where you need the callback.
-(void)didEndReloading:(UITableView *)tableView{
//do your stuff.
}
Reference: https://discussions.apple.com/thread/2598339?start=0&tstart=0
I am copying Andrew's code and expanding it to account for the case where you just have 1 row in the table. It's working so far for me!
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// detect when all visible cells have been loaded and displayed
// NOTE: iOS7 workaround used - see: http://stackoverflow.com/questions/4163579/how-to-detect-the-end-of-loading-of-uitableview?lq=1
NSArray *visibleRows = [tableView indexPathsForVisibleRows];
NSIndexPath *lastVisibleCellIndexPath = [visibleRows lastObject];
BOOL isPreviousCallForPreviousCell = self.previousDisplayedIndexPath.row + 1 == lastVisibleCellIndexPath.row;
BOOL isLastCell = [indexPath isEqual:lastVisibleCellIndexPath];
BOOL isFinishedLoadingTableView = isLastCell && ([tableView numberOfRowsInSection:0] == 1 || isPreviousCallForPreviousCell);
self.previousDisplayedIndexPath = indexPath;
if (isFinishedLoadingTableView) {
[self hideSpinner];
}
}
NOTE: I'm just using 1 section from Andrew's code, so keep that in mind..
#folex answer is right.
But it will fail if the tableView has more than one section displayed at a time.
-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if([indexPath isEqual:((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject])]){
//end of loading
}
}
In Swift you can do something like this. Following condition will be true every time you reach end of the tableView
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row+1 == postArray.count {
println("came to last row")
}
}
If you have multiple sections, here's how to get the last row in the last section (Swift 3):
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let visibleRows = tableView.indexPathsForVisibleRows, let lastRow = visibleRows.last?.row, let lastSection = visibleRows.map({$0.section}).last {
if indexPath.row == lastRow && indexPath.section == lastSection {
// Finished loading visible rows
}
}
}
Quite accidentally I bumped into this solution:
tableView.tableFooterView = UIView()
tableViewHeight.constant = tableView.contentSize.height
You need to set the footerView before getting the contentSize e.g. in viewDidLoad.
Btw. setting the footeView lets you delete "unused" separators
UITableView + Paging enable AND calling scrollToRow(..) to start on that page.
Best ugly workaround so far :/
override func viewDidLoad() {
super.viewDidLoad()
<#UITableView#>.reloadData()
<#IUTableView#>.alpha = .zero
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.<#IUTableView#>.scrollToRow(at: <#IndexPath#>, at: .none, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self?.<#IUTableView#>.alpha = 1
}
}
}
Are you looking for total number of items that will be displayed in the table or total of items currently visible? Either way.. I believe that the 'viewDidLoad' method executes after all the datasource methods are called. However, this will only work on the first load of the data(if you are using a single alloc ViewController).
I know this is answered, I am just adding a recommendation.
As per the following documentation
https://www.objc.io/issues/2-concurrency/thread-safe-class-design/
Fixing timing issues with dispatch_async is a bad idea. I suggest we should handle this by adding FLAG or something.
In iOS7.0x the solution is a bit different. Here is what I came up with.
- (void)tableView:(UITableView *)tableView
willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath
{
BOOL isFinishedLoadingTableView = [self isFinishedLoadingTableView:tableView
indexPath:indexPath];
if (isFinishedLoadingTableView) {
NSLog(#"end loading");
}
}
- (BOOL)isFinishedLoadingTableView:(UITableView *)tableView
indexPath:(NSIndexPath *)indexPath
{
// The reason we cannot just look for the last row is because
// in iOS7.0x the last row is updated before
// looping through all the visible rows in ascending order
// including the last row again. Strange but true.
NSArray * visibleRows = [tableView indexPathsForVisibleRows]; // did verify sorted ascending via logging
NSIndexPath *lastVisibleCellIndexPath = [visibleRows lastObject];
// For tableviews with multiple sections this will be more complicated.
BOOL isPreviousCallForPreviousCell =
self.previousDisplayedIndexPath.row + 1 == lastVisibleCellIndexPath.row;
BOOL isLastCell = [indexPath isEqual:lastVisibleCellIndexPath];
BOOL isFinishedLoadingTableView = isLastCell && isPreviousCallForPreviousCell;
self.previousDisplayedIndexPath = indexPath;
return isFinishedLoadingTableView;
}
Objective C
[self.tableView reloadData];
[self.tableView performBatchUpdates:^{}
completion:^(BOOL finished) {
/// table-view finished reload
}];
Swift
self.tableView?.reloadData()
self.tableView?.performBatchUpdates({ () -> Void in
}, completion: { (Bool finished) -> Void in
/// table-view finished reload
})

Resources