可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I am trying to scroll to the bottom of a UITableView after it is done performing [self.tableView reloadData]
I originally had
[self.tableView reloadData]
NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection: ([self.tableView numberOfSections]-1)];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
But then I read that reloadData is asynchronous, so the scrolling doesn\'t happen since the self.tableView
, [self.tableView numberOfSections]
and [self.tableView numberOfRowsinSection
are all 0.
Thanks!
What\'s weird is that I am using:
[self.tableView reloadData];
NSLog(@\"Number of Sections %d\", [self.tableView numberOfSections]);
NSLog(@\"Number of Rows %d\", [self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1);
In the console it returns Sections = 1, Row = -1;
When I do the exact same NSLogs in cellForRowAtIndexPath
I get Sections = 1 and Row = 8; (8 is right)
回答1:
The reload happens during the next layout pass, which normally happens when you return control to the run loop (after, say, your button action or whatever returns).
So one way to run something after the table view reloads is simply to force the table view to perform layout immediately:
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection: ([self.tableView numberOfSections]-1)];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
Another way is to schedule your after-layout code to run later using dispatch_async
:
[self.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection:([self.tableView numberOfSections]-1)];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
});
UPDATE
Upon further investigation, I find that the table view sends tableView:numberOfSections:
and tableView:numberOfRowsInSection:
to its data source before returning from reloadData
. If the delegate implements tableView:heightForRowAtIndexPath:
, the table view also sends that (for each row) before returning from reloadData
.
However, the table view does not send tableView:cellForRowAtIndexPath:
or tableView:headerViewForSection
until the layout phase, which happens by default when you return control to the run loop.
I also find that in a tiny test program, the code in your question properly scrolls to the bottom of the table view, without me doing anything special (like sending layoutIfNeeded
or using dispatch_async
).
回答2:
Swift:
extension UITableView {
func reloadData(completion: ()->()) {
UIView.animateWithDuration(0, animations: { self.reloadData() })
{ _ in completion() }
}
}
...somewhere later...
tableView.reloadData {
println(\"done\")
}
Objective-C:
[UIView animateWithDuration:0 animations:^{
[myTableView reloadData];
} completion:^(BOOL finished) {
//Do something after that...
}];
回答3:
As of Xcode 8.2.1, iOS 10, and swift 3,
You can determine the end of tableView.reloadData()
easily by using a CATransaction block:
CATransaction.begin()
CATransaction.setCompletionBlock({
print(\"reload completed\")
//Your completion code here
)}
print(\"reloading\")
tableView.reloadData()
CATransaction.commit()
The above also works for determining the end of UICollectionView\'s reloadData() and UIPickerView\'s reloadAllComponents().
回答4:
The dispatch_async(dispatch_get_main_queue())
method above is not guaranteed to work. I\'m seeing non-deterministic behavior with it, in which sometimes the system has completed the layoutSubviews and the cell rendering before the completion block, and sometimes after.
Here\'s a solution that works 100% for me, on iOS 10. It requires the ability to instantiate the UITableView or UICollectionView as a custom subclass. Here\'s the UICollectionView solution, but it\'s exactly the same for UITableView:
CustomCollectionView.h:
#import <UIKit/UIKit.h>
@interface CustomCollectionView: UICollectionView
- (void)reloadDataWithCompletion:(void (^)(void))completionBlock;
@end
CustomCollectionView.m:
#import \"CustomCollectionView.h\"
@interface CustomCollectionView ()
@property (nonatomic, copy) void (^reloadDataCompletionBlock)(void);
@end
@implementation CustomCollectionView
- (void)reloadDataWithCompletion:(void (^)(void))completionBlock
{
self.reloadDataCompletionBlock = completionBlock;
[super reloadData];
}
- (void)layoutSubviews
{
[super layoutSubviews];
if (self.reloadDataCompletionBlock) {
self.reloadDataCompletionBlock();
self.reloadDataCompletionBlock = nil;
}
}
@end
Example usage:
[self.collectionView reloadDataWithCompletion:^{
// reloadData is guaranteed to have completed
}];
See here for a Swift version of this answer
回答5:
I had the same issues as Tyler Sheaffer.
I implemented his solution in Swift and it solved my problems.
Swift 3.0:
final class UITableViewWithReloadCompletion: UITableView {
private var reloadDataCompletionBlock: (() -> Void)?
override func layoutSubviews() {
super.layoutSubviews()
reloadDataCompletionBlock?()
reloadDataCompletionBlock = nil
}
func reloadDataWithCompletion(completion: @escaping () -> Void) {
reloadDataCompletionBlock = completion
super.reloadData()
}
}
Swift 2:
class UITableViewWithReloadCompletion: UITableView {
var reloadDataCompletionBlock: (() -> Void)?
override func layoutSubviews() {
super.layoutSubviews()
self.reloadDataCompletionBlock?()
self.reloadDataCompletionBlock = nil
}
func reloadDataWithCompletion(completion:() -> Void) {
reloadDataCompletionBlock = completion
super.reloadData()
}
}
Example Usage:
tableView.reloadDataWithCompletion() {
// reloadData is guaranteed to have completed
}
回答6:
It appears folks are still reading this question and the answers. B/c of that, I\'m editing my answer to remove the word Synchronous which is really irrelevant to this.
When [tableView reloadData]
returns, the internal data structures behind the tableView have been updated. Therefore, when the method completes you can safely scroll to the bottom. I verified this in my own app. The widely accepted answer by @rob-mayoff, while also confusing in terminology, acknowledges the same in his last update.
If your tableView
isn\'t scrolling to the bottom you may have an issue in other code you haven\'t posted. Perhaps you are changing data after scrolling is complete and you\'re not reloading and/or scrolling to the bottom then?
Add some logging as follows to verify that the table data is correct after reloadData
. I have the following code in a sample app and it works perfectly.
// change the data source
NSLog(@\"Before reload / sections = %d, last row = %d\",
[self.tableView numberOfSections],
[self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]);
[self.tableView reloadData];
NSLog(@\"After reload / sections = %d, last row = %d\",
[self.tableView numberOfSections],
[self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]);
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]-1
inSection:[self.tableView numberOfSections] - 1]
atScrollPosition:UITableViewScrollPositionBottom
animated:YES];
回答7:
And a UICollectionView
version, based on kolaworld\'s answer:
https://stackoverflow.com/a/43162226/1452758
Needs testing. Works so far on iOS 9.2, Xcode 9.2 beta 2, with scrolling a collectionView to an index, as a closure.
extension UICollectionView
{
/// Calls reloadsData() on self, and ensures that the given closure is
/// called after reloadData() has been completed.
///
/// Discussion: reloadData() appears to be asynchronous. i.e. the
/// reloading actually happens during the next layout pass. So, doing
/// things like scrolling the collectionView immediately after a
/// call to reloadData() can cause trouble.
///
/// This method uses CATransaction to schedule the closure.
func reloadDataThenPerform(_ closure: @escaping (() -> Void))
{
CATransaction.begin()
CATransaction.setCompletionBlock(closure)
self.reloadData()
CATransaction.commit()
}
}
Usage:
myCollectionView.reloadDataThenPerform {
myCollectionView.scrollToItem(at: indexPath,
at: .centeredVertically,
animated: true)
}
回答8:
I use this trick, pretty sure I already posted it to a duplicate of this question:
-(void)tableViewDidLoadRows:(UITableView *)tableView{
// do something after loading, e.g. select a cell.
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// trick to detect when table view has finished loading.
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tableViewDidLoadRows:) object:tableView];
[self performSelector:@selector(tableViewDidLoadRows:) withObject:tableView afterDelay:0];
// specific to your controller
return self.objects.count;
}
回答9:
Actually this one solved my problem:
-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
NSSet *visibleSections = [NSSet setWithArray:[[tableView indexPathsForVisibleRows] valueForKey:@\"section\"]];
if (visibleSections) {
// hide the activityIndicator/Loader
}}
回答10:
Try this way it will work
[tblViewTerms performSelectorOnMainThread:@selector(dataLoadDoneWithLastTermIndex:) withObject:lastTermIndex waitUntilDone:YES];waitUntilDone:YES];
@interface UITableView (TableViewCompletion)
-(void)dataLoadDoneWithLastTermIndex:(NSNumber*)lastTermIndex;
@end
@implementation UITableView(TableViewCompletion)
-(void)dataLoadDoneWithLastTermIndex:(NSNumber*)lastTermIndex
{
NSLog(@\"dataLoadDone\");
NSIndexPath* indexPath = [NSIndexPath indexPathForRow: [lastTermIndex integerValue] inSection: 0];
[self selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
}
@end
I will execute when table is completely loaded
Other Solution is you can subclass UITableView
回答11:
I ended up using a variation of Shawn\'s solution:
Create a custom UITableView class with a delegate:
protocol CustomTableViewDelegate {
func CustomTableViewDidLayoutSubviews()
}
class CustomTableView: UITableView {
var customDelegate: CustomTableViewDelegate?
override func layoutSubviews() {
super.layoutSubviews()
self.customDelegate?.CustomTableViewDidLayoutSubviews()
}
}
Then in my code, I use
class SomeClass: UIViewController, CustomTableViewDelegate {
@IBOutlet weak var myTableView: CustomTableView!
override func viewDidLoad() {
super.viewDidLoad()
self.myTableView.customDelegate = self
}
func CustomTableViewDidLayoutSubviews() {
print(\"didlayoutsubviews\")
// DO other cool things here!!
}
}
Also make sure you set your table view to CustomTableView in the interface builder:
回答12:
In Swift 3.0 + we can create a an extension for UITableView
with a escaped Closure
like below :
extension UITableView {
func reloadData(completion: @escaping () -> ()) {
UIView.animate(withDuration: 0, animations: { self.reloadData()})
{_ in completion() }
}
}
And Use it like Below where ever you want :
Your_Table_View.reloadData {
print(\"reload done\")
}
hope this will help to someone. cheers!
回答13:
You can use it for do something after reload data:
[UIView animateWithDuration:0 animations:^{
[self.contentTableView reloadData];
} completion:^(BOOL finished) {
_isUnderwritingUpdate = NO;
}];
回答14:
Try setting delays:
[_tableView performSelector:@selector(reloadData) withObject:nil afterDelay:0.2];
[_activityIndicator performSelector:@selector(stopAnimating) withObject:nil afterDelay:0.2];