UITableView が [self.tableView reloadData]
を実行した後に、UITableView の一番下までスクロールしようとしています。
私はもともと
[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];
しかし、その後、reloadDataは非同期であると読み、self.tableView
, [self.tableView numberOfSections]
, [self.tableView numberOfRowsinSection
がすべて0であるため、スクロールは起こりません。
ありがとうございます!(笑
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);
コンソールでは、Section = 1, Row = -1 が返されます。
cellForRowAtIndexPath`で全く同じNSLogを実行すると、Sections = 1, Row = 8; (8が正しい)となります。
リロードは次のレイアウトパスの間に行われ、通常はランループに制御を戻したときに起こります(例えば、ボタンアクションなどが戻った後です)。
そのため、テーブルビューの再読み込み後に何かを実行する方法の1つとして、単純にテーブルビューにレイアウトを即座に実行させることができます。
[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];
もう一つの方法は、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];
});
さらに調べてみると、テーブルビューは reloadData
から戻る前に tableView:numberOfSections:
と tableView:numberOfRowsInSection:
をデータソースに送信することがわかりました。 デリゲートが tableView:heightForRowAtIndexPath:
を実装している場合、テーブルビューは reloadData
から戻る前にそれを(各行に対して)送信します。
しかし、テーブルビューは tableView:cellForRowAtIndexPath:
や tableView:headerViewForSection
をレイアウトフェーズまで送信しません(デフォルトでは、ランループに制御を戻したときに起こります)。
また、小さなテストプログラムでは、ご質問のコードは、私が特別なことをしなくても(layoutIfNeeded
を送信したり、dispatch_async
を使用したり)、テーブルビューの一番下まで適切にスクロールすることがわかりました。
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...
}];
Xcode 8.2.1、iOS 10、およびswift 3以降。
CATransactionブロックを使用して、 tableView.reloadData()
の終わりを簡単に決定できます。
CATransaction.begin()
CATransaction.setCompletionBlock({
print("reload completed")
//Your completion code here
})
print("reloading")
tableView.reloadData()
CATransaction.commit()
上記は、UICollectionViewのリロードData()およびUIPickerViewのリロードAllComponents()の終了を決定するためにも機能します。
上記の dispatch_async(dispatch_get_main_queue())
メソッドは動作が保証されていませんです。 私はそれで非決定論的な行動を見ています。そこでは、システムがlaightSubviewsとセルレンダリングを完了してから完了ブロック前、そして場合によっては後で完了することがあります。
iOS 10で、私にとって100%機能するソリューションを次に示します。 UITableViewまたはUICollectionViewをカスタムサブクラスとしてインスタンス化する機能が必要です。 これがUICollectionViewソリューションですが、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;
[self reloadData];
}
- (void)layoutSubviews
{
[super layoutSubviews];
if (self.reloadDataCompletionBlock) {
self.reloadDataCompletionBlock();
self.reloadDataCompletionBlock = nil;
}
}
@end
使用例:
[self.collectionView reloadDataWithCompletion:^{
// reloadData is guaranteed to have completed
}];
この回答のSwiftバージョンについては、ここを参照してください。
私はタイラー・シェファーと同じ問題を抱えていました。
Swiftで彼のソリューションを実装したところ、問題が解決しました。
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
self.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
self.reloadData()
}
}
使用例:
tableView.reloadDataWithCompletion() {
// reloadData is guaranteed to have completed
}
皆さんはまだこの質問と回答を読んでいるようですね。そのため、私は自分の回答を編集して、この質問とは無関係なSynchronousという単語を削除しています。
tableView reloadData]`が返されるとき、tableViewの背後にある内部データ構造は更新されています。したがって、このメソッドが完了したときに、安全に下までスクロールすることができます。私はこれを自分のアプリで検証しました。広く受け入れられている@rob-mayoff氏の回答も、用語が紛らわしいものの、前回の更新で同じことを認めています。
もしtableView
が下までスクロールしないのであれば、あなたが投稿していない他のコードに問題があるのかもしれません。おそらく、スクロールが完了した後にデータを変更していて、その時に再読み込みや下へのスクロールが行われていないのでは?
以下のようにロギングを追加して、reloadData
後にテーブルデータが正しいかどうかを確認します。私はサンプルアプリで以下のコードを持っていますが、完全に動作しています。
// 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];
そして、kolaworldの答えに基づく「UICollectionView」バージョン:
https://stackoverflow.com/a/43162226/1452758。
テストが必要です。 これまでのところ、iOS 9.2、Xcode 9.2ベータ2で動作し、closesとしてcollectionViewをインデックスにスクロールします。
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()
}
}
使用法:。
myCollectionView.reloadDataThenPerform {
myCollectionView.scrollToItem(at: indexPath,
at: .centeredVertically,
animated: true)
}
私はこのトリックを使用します。この質問の複製にすでに投稿したことを確信しています。
-(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;
}
実際、これは私の問題を解決しました。
-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
NSSet *visibleSections = [NSSet setWithArray:[[tableView indexPathsForVisibleRows] valueForKey:@"section"]];
if (visibleSections) {
// hide the activityIndicator/Loader
}}
この方法で試してみてください。
[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
テーブルが完全に読み込まれたときに実行します。
その他の解決策は、UITableViewをサブクラス化できることです。
<。!-言語-すべて:Swift-->。
私はショーンのソリューションのバリエーションを使用してしまいました:
デリゲートを使用してカスタムUITableViewクラスを作成します。
protocol CustomTableViewDelegate {
func CustomTableViewDidLayoutSubviews()
}
class CustomTableView: UITableView {
var customDelegate: CustomTableViewDelegate?
override func layoutSubviews() {
super.layoutSubviews()
self.customDelegate?.CustomTableViewDidLayoutSubviews()
}
}
次に、コードで使用します。
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!!
}
}
また、インターフェイスビルダーでテーブルビューをCustomTableViewに設定してください。
。。
Swift 3.0 +では、以下のような「エスケープされたクロージャー」を使用して、「UITableView」の拡張機能を作成できます。
extension UITableView {
func reloadData(completion: @escaping () -> ()) {
UIView.animate(withDuration: 0, animations: { self.reloadData()})
{_ in completion() }
}
}
そして、あなたが望むところならどこでもそれを使用してください:。
Your_Table_View.reloadData {
print("reload done")
}
これが誰かに役立つことを願っています。 乾杯。!。
-Xcodeバージョン10.2.1(10E1001)、Swift 5。
import UIKit
// MARK: - UITableView reloading functions
protocol ReloadCompletable: class { func reloadData() }
extension ReloadCompletable {
func run(transaction closure: (() -> Void)?, completion: (() -> Void)?) {
guard let closure = closure else { return }
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
closure()
CATransaction.commit()
}
func run(transaction closure: (() -> Void)?, completion: ((Self) -> Void)?) {
run(transaction: closure) { [weak self] in
guard let self = self else { return }
completion?(self)
}
}
func reloadData(completion closure: ((Self) -> Void)?) {
run(transaction: { [weak self] in self?.reloadData() }, completion: closure)
}
}
// MARK: - UITableView reloading functions
extension ReloadCompletable where Self: UITableView {
func reloadRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation, completion closure: ((Self) -> Void)?) {
run(transaction: { [weak self] in self?.reloadRows(at: indexPaths, with: animation) }, completion: closure)
}
func reloadSections(_ sections: IndexSet, with animation: UITableView.RowAnimation, completion closure: ((Self) -> Void)?) {
run(transaction: { [weak self] in self?.reloadSections(sections, with: animation) }, completion: closure)
}
}
// MARK: - UICollectionView reloading functions
extension ReloadCompletable where Self: UICollectionView {
func reloadSections(_ sections: IndexSet, completion closure: ((Self) -> Void)?) {
run(transaction: { [weak self] in self?.reloadSections(sections) }, completion: closure)
}
func reloadItems(at indexPaths: [IndexPath], completion closure: ((Self) -> Void)?) {
run(transaction: { [weak self] in self?.reloadItems(at: indexPaths) }, completion: closure)
}
}
UITableView。
// Activate
extension UITableView: ReloadCompletable { }
// ......
let tableView = UICollectionView()
// reload data
tableView.reloadData { tableView in print(collectionView) }
// or
tableView.reloadRows(at: indexPathsToReload, with: rowAnimation) { tableView in print(tableView) }
// or
tableView.reloadSections(IndexSet(integer: 0), with: rowAnimation) { _tableView in print(tableView) }
UICollectionView。
// Activate
extension UICollectionView: ReloadCompletable { }
// ......
let collectionView = UICollectionView()
// reload data
collectionView.reloadData { collectionView in print(collectionView) }
// or
collectionView.reloadItems(at: indexPathsToReload) { collectionView in print(collectionView) }
// or
collectionView.reloadSections(IndexSet(integer: 0)) { collectionView in print(collectionView) }
ここにソリューションコードを追加することを忘れないでください。
import UIKit
class ViewController: UIViewController {
private weak var navigationBar: UINavigationBar?
private weak var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
setupNavigationItem()
setupTableView()
}
}
// MARK: - Activate UITableView reloadData with completion functions
extension UITableView: ReloadCompletable { }
// MARK: - Setup(init) subviews
extension ViewController {
private func setupTableView() {
guard let navigationBar = navigationBar else { return }
let tableView = UITableView()
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.dataSource = self
self.tableView = tableView
}
private func setupNavigationItem() {
let navigationBar = UINavigationBar()
view.addSubview(navigationBar)
self.navigationBar = navigationBar
navigationBar.translatesAutoresizingMaskIntoConstraints = false
navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
navigationBar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
let navigationItem = UINavigationItem()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "all", style: .plain, target: self, action: #selector(reloadAllCellsButtonTouchedUpInside(source:)))
let buttons: [UIBarButtonItem] = [
.init(title: "row", style: .plain, target: self,
action: #selector(reloadRowButtonTouchedUpInside(source:))),
.init(title: "section", style: .plain, target: self,
action: #selector(reloadSectionButtonTouchedUpInside(source:)))
]
navigationItem.leftBarButtonItems = buttons
navigationBar.items = [navigationItem]
}
}
// MARK: - Buttons actions
extension ViewController {
@objc func reloadAllCellsButtonTouchedUpInside(source: UIBarButtonItem) {
let elementsName = "Data"
print("-- Reloading \(elementsName) started")
tableView?.reloadData { taleView in
print("-- Reloading \(elementsName) stopped \(taleView)")
}
}
private var randomRowAnimation: UITableView.RowAnimation {
return UITableView.RowAnimation(rawValue: (0...6).randomElement() ?? 0) ?? UITableView.RowAnimation.automatic
}
@objc func reloadRowButtonTouchedUpInside(source: UIBarButtonItem) {
guard let tableView = tableView else { return }
let elementsName = "Rows"
print("-- Reloading \(elementsName) started")
let indexPathToReload = tableView.indexPathsForVisibleRows?.randomElement() ?? IndexPath(row: 0, section: 0)
tableView.reloadRows(at: [indexPathToReload], with: randomRowAnimation) { _tableView in
//print("-- \(taleView)")
print("-- Reloading \(elementsName) stopped in \(_tableView)")
}
}
@objc func reloadSectionButtonTouchedUpInside(source: UIBarButtonItem) {
guard let tableView = tableView else { return }
let elementsName = "Sections"
print("-- Reloading \(elementsName) started")
tableView.reloadSections(IndexSet(integer: 0), with: randomRowAnimation) { _tableView in
//print("-- \(taleView)")
print("-- Reloading \(elementsName) stopped in \(_tableView)")
}
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int { return 1 }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 20 }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "\(Date())"
return cell
}
}
。。
別のアプローチを提供するためだけに、完了は「cellForRow」に送信される「最後に表示される」セルであるという考えに基づいています。
// Will be set when reload is called
var lastIndexPathToDisplay: IndexPath?
typealias ReloadCompletion = ()->Void
var reloadCompletion: ReloadCompletion?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Setup cell
if indexPath == self.lastIndexPathToDisplay {
self.lastIndexPathToDisplay = nil
self.reloadCompletion?()
self.reloadCompletion = nil
}
// Return cell
...
func reloadData(completion: @escaping ReloadCompletion) {
self.reloadCompletion = completion
self.mainTable.reloadData()
self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last
}
---。
考えられる問題の1つは、「lastIndexPathToDisplay」が設定される前に「reloadData()」が終了した場合、「lastIndexPathToDisplay」が設定される前に「最後に表示される」セルが表示され、完了が呼び出されない(そして「待機」状態):
self.mainTable.reloadData()
// cellForRowAt could be finished here, before setting `lastIndexPathToDisplay`
self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last
逆にすると、「reloadData()」の前にスクロールすることで完了がトリガーされる可能性があります。
self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last
// cellForRowAt could trigger the completion by scrolling here since we arm 'lastIndexPathToDisplay' before 'reloadData()'
self.mainTable.reloadData()