web-dev-qa-db-ja.com

上部に行を挿入するときにuitableviewを静的に保つ

上部に行を挿入するtableViewがあります。

これを実行している間、現在のビューを完全に静止したままにするため、上にスクロールした場合にのみ行が表示されます。

基になるUIScrollviewの現在の位置を保存し、行が挿入された後に位置をリセットしようとしましたが、同じ場所に戻ってしまいますが、これにより上下にジャダーが発生します。

これを達成する良い方法はありますか?

更新:beginUpdateを使用してから、insertRowsAtIndexPath、endUpdatesを使用しています。 reloadData呼び出しはありません。

scrollToRowAtIndexPathは、行を追加する前に保存されている現在のセルの最上部にジャンプします。

私が試したもう1つのアプローチは、最終的に正確に正しいペースになりますが、ジャダーがあります。

save tableView currentOffset. (Underlying scrollView method)
Add rows (beginUpdates,insert...,endUpdates) 
reloadData ( to force a recalulation of the scrollview size )
Recalculate the correct new offset from the bottom of the scrollview
setContentOffset (Underlying scrollview method)

問題は、reloadDataがscrollview/tableviewを短時間スクロールさせ、その後setContentOffsetがそれを正しい場所に戻すことです。

表示を開始せずにtableViewを取得して新しいサイズにする方法はありますか?

BeginAnimation commitAnimationですべてをラップしても、あまり役に立ちません。

更新2:これは明らかにできます-更新のためにプルダウンするとき、公式のTwitterアプリをご覧ください。

60
Dean Smith

実際にすべての行の高さを合計する必要はありません。テーブルをリロードした後の新しいcontentSizeはすでにそれを表しています。したがって、コンテンツサイズの高さのデルタを計算し、それを現在のオフセットに追加するだけです。

    ...
    CGSize beforeContentSize = self.tableView.contentSize;
    [self.tableView reloadData];
    CGSize afterContentSize = self.tableView.contentSize;

    CGPoint afterContentOffset = self.tableView.contentOffset;
    CGPoint newContentOffset = CGPointMake(afterContentOffset.x, afterContentOffset.y + afterContentSize.height - beforeContentSize.height);
    self.tableView.contentOffset = newContentOffset;
    ...
48
AmitP
-(void) updateTableWithNewRowCount : (int) rowCount
{    
    //Save the tableview content offset 
    CGPoint tableViewOffset = [self.tableView contentOffset];                                                                                                            

    //Turn of animations for the update block 
    //to get the effect of adding rows on top of TableView
    [UIView setAnimationsEnabled:NO];

    [self.tableView beginUpdates];                    

    NSMutableArray *rowsInsertIndexPath = [[NSMutableArray alloc] init];        

    int heightForNewRows = 0;

    for (NSInteger i = 0; i < rowCount; i++) {

        NSIndexPath *tempIndexPath = [NSIndexPath indexPathForRow:i inSection:SECTION_TO_INSERT];
        [rowsInsertIndexPath addObject:tempIndexPath];

        heightForNewRows = heightForNewRows + [self heightForCellAtIndexPath:tempIndexPath];                        
    }

    [self.tableView insertRowsAtIndexPaths:rowsInsertIndexPath withRowAnimation:UITableViewRowAnimationNone];                                                                            

    tableViewOffset.y += heightForNewRows;                                                                                 

    [self.tableView endUpdates]; 

    [UIView setAnimationsEnabled:YES];

    [self.tableView setContentOffset:tableViewOffset animated:NO];           
}


-(int) heightForCellAtIndexPath: (NSIndexPath *) indexPath
{

    UITableViewCell *cell =  [self.tableView cellForRowAtIndexPath:indexPath];

    int cellHeight   =  cell.frame.size.height;

    return cellHeight;
}

新しい行の行数を渡すだけで、上部に挿入できます。

24
Mayank Yadav

@Deanの画像キャッシュの使用方法はあまりにもハック的で、UIの応答性を損なうと思います。

1つの適切な方法:UITableViewサブクラスを使用し、-setContentSize:をオーバーライドします。この方法では、contentOffsetを設定することで、テーブルビューが押し下げられる量を計算できます。

これは、すべての挿入がテーブルビューの上部で発生する最も単純な状況を処理するための最も単純なサンプルコードです。

@implementation MyTableView

- (void)setContentSize:(CGSize)contentSize {
        // I don't want move the table view during its initial loading of content.
    if (!CGSizeEqualToSize(self.contentSize, CGSizeZero)) {
        if (contentSize.height > self.contentSize.height) {
            CGPoint offset = self.contentOffset;
            offset.y += (contentSize.height - self.contentSize.height);
            self.contentOffset = offset;
        }
    }
    [super setContentSize:contentSize];
}

@end
21
an0

同じ問題があり、解決策を見つけました。

    save tableView currentOffset. (Underlying scrollView method)
    //Add rows (beginUpdates,insert...,endUpdates) // don't do this!
    reloadData ( to force a recalulation of the scrollview size )
    add newly inserted row heights to contentOffset.y here, using tableView:heightForRowAtIndexPath:
    setContentOffset (Underlying scrollview method)

このような:

- (CGFloat) firstRowHeight
{
    return [self tableView:[self tableView] heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
}

...
CGPoint offset = [[self tableView] contentOffset];
[self tableView] reloadData];
offset.y += [self firstRowHeight];
if (offset.y > [[self tableView] contentSize].height) {
    offset.y = 0;
}
[[self tableView] setContentOffset:offset];
...

グリッチなしで完全に動作します。

17
Andrey Zverev

コアデータサンプルプロジェクトを使用していくつかのテストを行い、表示されている最上部のセルの上に新しいセルが追加されている間、それを静止させました。このコードは、画面に空きスペースがあるテーブル用に調整する必要がありますが、画面がいっぱいになると正常に機能します。

static CGPoint  delayOffset = {0.0};

- (void)controllerWillChangeContent:(NSFetchedResultsController*)controller {
    if ( animateChanges )
        [self.tableView beginUpdates];
    delayOffset = self.tableView.contentOffset; // get the current scroll setting
}

これをセル挿入ポイントに追加しました。セルの削除については、対応する減算を行うことができます。

    case NSFetchedResultsChangeInsert:

        delayOffset.y += self.tableView.rowHeight;  // add for each new row
        if ( animateChanges )   
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationNone];
        break;

そして最後に

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    if ( animateChanges )   
    {
        [self.tableView setContentOffset:delayOffset animated:YES];
        [self.tableView endUpdates];
    }
    else
    {
        [self.tableView reloadData];
        [self.tableView setContentOffset:delayOffset animated:NO];
    }
}

AnimateChanges = NOを使用すると、セルが追加されたときに何も動くことがありませんでした。

AnimateChanges = YESでのテストでは、「ジャダー」がありました。セル挿入のアニメーションは、アニメーション化されたテーブルのスクロールと同じ速度ではなかったようです。最後の結果は、セルが開始された場所とまったく同じように見えるセルで終了する可能性がありますが、テーブル全体が2ピクセルまたは3ピクセル移動してから戻るように見えます。

アニメーションの速度を等しくすることができれば、そのままに見えるかもしれません。

ただし、前のアニメーションが終了する前にボタンを押して行を追加すると、アニメーションが突然停止してから次のアニメーションが開始され、位置が急激に変化します。

9
Walt Sellers

@Dean、

このようにコードを変更して、アニメーションを防ぐことができます。

[tableView beginUpdates];
[UIView setAnimationsEnabled:NO];

// ...

[tableView endUpdates];

[tableView setContentOffset:newOffset animated:NO];
[UIView setAnimationsEnabled:YES];
5
Norris Tong

コード例をコピーして貼り付けることは誰もが大好きなので、ここにAndrey Z.の答えの実装があります。

これは私のdelegateDidFinishUpdating:(MyDataSourceDelegate*)delegateメソッドにあります

if (self.contentOffset.y <= 0)
{
    [self beginUpdates];
    [self insertRowsAtIndexPaths:insertedIndexPaths withRowAnimation:insertAnimation];
    [self endUpdates];
}
else
{
    CGPoint newContentOffset = self.contentOffset;
    [self reloadData];

    for (NSIndexPath *indexPath in insertedIndexPaths)
        newContentOffset.y += [self.delegate tableView:self heightForRowAtIndexPath:indexPath];

    [self setContentOffset:newContentOffset];

    NSLog(@"New data at top of table view");
}

下部のNSLogは、新しいデータがあることを示すビューを表示するための呼び出しに置き換えることができます。

4
mikezs

カスタムグループ化のために-reloadData呼び出し間で行数が異なるセクションが多数あり、行の高さが異なる状況に直面しました。 AndreyZをベースにしたソリューションです。 -reloadDataの前後のUIScrollViewのcontentHeightプロパティであり、より普遍的なようです。

CGFloat contentHeight = self.tableView.contentSize.height;
CGPoint offset = self.tableView.contentOffset;
[self.tableView reloadData];

offset.y += (self.tableView.contentSize.height - contentHeight);
if (offset.y > [self.tableView contentSize].height)
    offset.y = 0;

[self.tableView setContentOffset:offset];

さらに条件を追加します。 iOS11以上のコードの場合、以下のようにする必要があります。

IOS 11では、テーブルビューはデフォルトで推定高さを使用します。これは、contentSizeが最初の推定値と同じであることを意味します。 contentSizeを使用する必要がある場合は、3つの推定高さプロパティをゼロに設定して、推定高さを無効にする必要があります。

tableView.estimatedRowHeight = 0 tableView.estimatedSectionHeaderHeight = 0 tableView.estimatedSectionFooterHeight = 0

2
pusswzy

テーブルに行をどのように追加しますか?

データソースを変更してからreloadDataを呼び出すと、テーブルが再び一番上にスクロールされる可能性があります。

ただし、beginUpdatesinsertRowsAtIndexPaths:withRowAnimation:endUpdatesメソッドを使用すると、reloadDataを呼び出さずに行を挿入できるため、テーブルを元の位置に保持できます。

endUpdatesを呼び出す前にデータソースを変更することを忘れないでください。そうしないと、内部不整合例外が発生します。

1
Jasarien

テーブルビューの推定高さを返す場合、これはできません。

   - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath ;

このメソッドを実装し、大まかな高さを返すと、オフセットを設定するときにこれらの高さを使用しているように見えるため、リロード時にTableViewがジャンプします。

上記の回答のいずれかを使用して動作させるには(@Mayank Yadav answerを使用しました)、timatedHeightメソッドを実装せず、セルの高さをキャッシュしないでください(上部に追加のセルを挿入するときにキャッシュを調整することを忘れないでください)。

1
bencallis

アニメーションを無効にする簡単なソリューション

func addNewRows(indexPaths: [NSIndexPath]) {
    let addBlock = { () -> Void in
        self.tableView.beginUpdates()
        self.tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .None)
        self.tableView.endUpdates()
    }

    tableView.contentOffset.y >= tableView.height() ? UIView.performWithoutAnimation(addBlock) : addBlock()
}
1
Sahil Kapoor

それほど難しい操作を行う必要はありません。さらに、これらの操作は完全には機能しません。簡単な解決策は、テーブルビューを回転させてから、そこにセルを回転させることです。

_tableView.transform = CGAffineTransformMakeRotation(M_PI);

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{

   cell.transform = CGAffineTransformMakeRotation(M_PI);

}
_

[tableView setScrollIndicatorInsets:UIEdgeInsetsMake(0, 0, 0, 310)]を使用して、スクロールインジケータの相対位置を設定します。テーブルビューの回転後、右側になります。

1

最後に、現在のテーブルビューをUIImageにレンダリングし、アニメーション化しながらテーブルビューの上に一時的なUIImageViewを置くことでこれを解決しました。

次のコードは画像を生成します


// Save the current tableView as an UIImage
CSize pageSize = [[self tableView] frame].size;
UIGraphicsBeginImageContextWithOptions(pageSize, YES, 0.0); // 0.0 means scale appropriate for device ( retina or no )
CGContextRef resizedContext = UIGraphicsGetCurrentContext();
CGPoint offset = [[self tableView] contentOffset];
CGContextTranslateCTM(resizedContext,-(offset.x),-(offset.y));

[[[self tableView  ]layer] renderInContext:resizedContext];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();    

行を挿入しながら、TableViewがどれだけ成長したかを追跡し、TableViewをスクロールして正確に同じ位置に戻す必要があります。

0
Dean Smith

パーティーに遅れましたが、これはセルに動的な高さ(別名UITableViewAutomaticDimension)があり、サイズを計算するためにセルを繰り返し処理する必要がない場合でも機能しますが、tableViewの先頭にアイテムが追加された場合にのみ機能しますはヘッダーではなく、少しの数学で、これをあらゆる状況に適応させることがおそらく可能です:

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
        if indexPath.row == 0 {
            self.getMoreMessages()
        }
}

private func getMoreMessages(){
        var initialOffset = self.tableView.contentOffset.y
        self.tableView.reloadData()
        //@numberOfCellsAdded: number of items added at top of the table 
        self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: numberOfCellsAdded, inSection: 0), atScrollPosition: .Top, animated: false)
        self.tableView.contentOffset.y += initialOffset
}
0
ZekeMidas

アンドレイ・Zの答えに基づいて、ここに私にとって完璧に機能するライブ例があります...

int numberOfRowsBeforeUpdate = [controller.tableView numberOfRowsInSection:0];
CGPoint currentOffset = controller.tableView.contentOffset;

if(numberOfRowsBeforeUpdate>0)
{
    [controller.tableView reloadData];
    int numberOfRowsAfterUpdate = [controller.tableView numberOfRowsInSection:0];

    float rowHeight = [controller getTableViewCellHeight];  //custom method in my controller
    float offset = (numberOfRowsAfterUpdate-numberOfRowsBeforeUpdate)*rowHeight;

    if(offset>0)
    {
        currentOffset.y = currentOffset.y+offset;
        [controller.tableView setContentOffset:currentOffset];
    }
}
else
    [controller.tableView reloadData];
0
boostedz06

AmitPが答え、Swift 3バージョン

let beforeContentSize = self.tableView.contentSize
self.tableView.reloadData()
let afterContentSize = self.tableView.contentSize
let afterContentOffset = self.tableView.contentOffset
let newContentOffset = CGPoint(x: afterContentOffset.x, y: afterContentOffset.y + afterContentSize.height - beforeContentSize.height)
                    self.tableView.contentOffset = newContentOffset
0
ddnl