1. ホーム
  2. ios

[解決済み] UITableViewのセル内にurlから非同期で画像を読み込むと、スクロール中に画像が間違った画像に変わってしまう

2022-04-24 11:50:21

質問

UITableViewのセル内に画像を非同期でロードする方法を2つ書きました。どちらの場合も、画像はうまくロードされますが、テーブルをスクロールすると、画像が数回変わり、スクロールが終わると画像は正しい画像に戻ります。なぜこのようなことが起こるのか、全く分かりません。

#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

- (void)viewDidLoad
{
    [super viewDidLoad];
    dispatch_async(kBgQueue, ^{
        NSData* data = [NSData dataWithContentsOfURL: [NSURL URLWithString:
                                                       @"http://myurl.com/getMovies.php"]];
        [self performSelectorOnMainThread:@selector(fetchedData:)
                               withObject:data waitUntilDone:YES];
    });
}

-(void)fetchedData:(NSData *)data
{
    NSError* error;
    myJson = [NSJSONSerialization
              JSONObjectWithData:data
              options:kNilOptions
              error:&error];
    [_myTableView reloadData];
}    

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // Return the number of sections.
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    // Return the number of rows in the section.
    // Usually the number of items in your array (the one that holds your list)
    NSLog(@"myJson count: %d",[myJson count]);
    return [myJson count];
}
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

        myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
        if (cell == nil) {
            cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
        }

        dispatch_async(kBgQueue, ^{
        NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];

            dispatch_async(dispatch_get_main_queue(), ^{
        cell.poster.image = [UIImage imageWithData:imgData];
            });
        });
         return cell;
}

... ...

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

            myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
            if (cell == nil) {
                cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
            }
    NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]];
    NSURLRequest* request = [NSURLRequest requestWithURL:url];


    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse * response,
                                               NSData * data,
                                               NSError * error) {
                               if (!error){
                                   cell.poster.image = [UIImage imageWithData:data];
                                   // do whatever you want with image
                               }

                           }];
     return cell;
}

解決方法は?

手っ取り早く戦術的な修正をしたいのであれば、セル画像が初期化されていることと、セルの行がまだ表示されていることを確認する必要があります、例.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];

    cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"];

    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg", self.myJson[indexPath.row][@"movieId"]]];

    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (data) {
            UIImage *image = [UIImage imageWithData:data];
            if (image) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
                    if (updateCell)
                        updateCell.poster.image = image;
                });
            }
        }
    }];
    [task resume];

    return cell;
}

上記のコードでは、セルが再利用されることに起因するいくつかの問題に対処しています。

  1. バックグランドリクエストを開始する前にセル画像を初期化していない(つまり、新しい画像をダウンロードしている間、キューから外されたセルの最後の画像がまだ表示されている)。必ず nil その image プロパティを使用しないと、画像のちらつきが発生します。

  2. もっと微妙な問題は、本当に遅いネットワークでは、セルが画面からスクロールする前に非同期リクエストが終了しないかもしれないことです。この場合 UITableView メソッド cellForRowAtIndexPath: (という名前がついています(似たような名前の UITableViewDataSource メソッド tableView:cellForRowAtIndexPath: ) を使用して、その行のセルがまだ表示されているかどうかを確認します。このメソッドは nil セルが表示されていない場合

    問題は、非同期メソッドが完了するまでにセルがスクロールしてしまい、さらに悪いことに、そのセルがテーブルの別の行に再利用されてしまっていることです。行がまだ表示されているかどうかを確認することで、誤って画面外にスクロールした行の画像で画像を更新しないようにします。

  3. この質問とは少し関係ありませんが、私は特に、現代の慣習とAPIを活用するためにこれを更新する必要に迫られました。

    • 使用方法 NSURLSession をディスパッチするのではなく -[NSData contentsOfURL:] をバックグラウンドのキューにコピーします。

    • 使用方法 dequeueReusableCellWithIdentifier:forIndexPath: よりも dequeueReusableCellWithIdentifier: (ただし、その識別子には必ずセルプロトタイプかレジスタークラスかNIBを使用すること); そして

    • に準拠したクラス名にしてみました。 Cocoaの命名規則 (つまり、大文字で始まる)。

これらの修正を行っても、問題はあります。

  1. 上記のコードでは、ダウンロードした画像をキャッシュしていません。つまり、画像を画面外にスクロールして画面内に戻した場合、アプリは再び画像を取得しようとするかもしれません。幸運なことに、サーバーのレスポンス・ヘッダが NSURLSessionNSURLCache しかし、そうでない場合は、不必要なサーバーリクエストを行い、より遅いUXを提供することになります。

  2. 画面外にスクロールするセルのリクエストはキャンセルしていません。したがって、100 行目まで急速にスクロールした場合、その行の画像は、もう表示されていない前の 99 行のリクエストの後ろに滞留している可能性があります。最高のUXを実現するためには、常に可視セルのリクエストを優先するようにします。

これらの問題を解決する最も簡単な方法は UIImageView で提供されているようなカテゴリです。 SDWebImage または AFNetworking . 必要であれば、自分でコードを書いて上記の問題に対処することもできますが、大変な作業ですし、上記のような UIImageView のカテゴリはすでにあなたのためにこれをやってくれています。