web-dev-qa-db-ja.com

UITableViewセル内のURLからの非同期画像ロード - スクロール中に画像が誤った画像に変わる

私は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;
}
147
Segev

手っ取り早い解決策を探しているとしたら、セルイメージが初期化され、セルの行がまだ表示されていることを確認する必要があります。

- (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を活用することを余儀なくされています。

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

    • dequeueReusableCellWithIdentifier:forIndexPath:ではなくdequeueReusableCellWithIdentifier:を使用してください(ただし、その識別子には必ずセルのプロトタイプ、レジスタクラス、またはNIBを使用してください)。そして

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

これらの修正でも、問題があります。

  1. 上記のコードはダウンロードした画像をキャッシュしていません。つまり、画像を画面外にスクロールして画面に戻すと、アプリはその画像を再度取得しようとします。おそらくあなたは、サーバのレスポンスヘッダがNSURLSessionNSURLCacheによって提供されるかなり透過的なキャッシングを許可してくれるかもしれませんが、そうでなければ、不要なサーバリクエストを行い、はるかに遅いUXを提供するでしょう。

  2. 画面外にスクロールするセルに対する要求はキャンセルされません。したがって、100行目まですばやくスクロールすると、その行の画像は、表示されなくなった前の99行の要求の後ろにバックログされる可能性があります。あなたは常に最高のUXのために見えるセルのリクエストを優先することを確認したいと思います。

これらの問題に対処する最も簡単な修正は、 SDWebImage または AFNetworking で提供されているように、UIImageViewカテゴリを使用することです。あなたが望むなら、あなたは上記の問題に対処するためにあなた自身のコードを書くことができますが、それは多くの仕事であり、そして上記のUIImageViewカテゴリはすでにあなたのためにこれをしています。

218
Rob

/ *私はこのようにそれをしました、そしてまたそれをテストしました* /

ステップ1 = viewDidLoadメソッドで、このようなテーブルにカスタムセルクラス(テーブル内のプロトタイプセルの場合)またはnib(カスタムセル用のカスタムnibの場合)を登録します。

[self.yourTableView registerClass:[CustomTableViewCell class] forCellReuseIdentifier:@"CustomCell"];

OR

[self.yourTableView registerNib:[UINib nibWithNibName:@"CustomTableViewCell" bundle:nil] forCellReuseIdentifier:@"CustomCell"];

ステップ2 = UITableViewの "dequeueReusableCellWithIdentifier:forIndexPath:"メソッドを次のように使用します(このためには、クラスまたはペン先を登録する必要があります)。

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

            cell.imageViewCustom.image = nil; // [UIImage imageNamed:@"default.png"];
            cell.textLabelCustom.text = @"Hello";

            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                // retrive image on global queue
                UIImage * img = [UIImage imageWithData:[NSData dataWithContentsOfURL:     [NSURL URLWithString:kImgLink]]];

                dispatch_async(dispatch_get_main_queue(), ^{

                    CustomTableViewCell * cell = (CustomTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
                  // assign cell image on main thread
                    cell.imageViewCustom.image = img;
                });
            });

            return cell;
        }
14
Nitesh Borad

この問題を解決するフレームワークは複数あります。ほんの数例を挙げると:

迅速:

目的C:

13
kean

スイフト3

私はNSCacheを使用してイメージローダーのための私自身の軽い実装を書きます。 セル画像のちらつきなし

ImageCacheLoader.Swift

typealias ImageCacheLoaderCompletionHandler = ((UIImage) -> ())

class ImageCacheLoader {

    var task: URLSessionDownloadTask!
    var session: URLSession!
    var cache: NSCache<NSString, UIImage>!

    init() {
        session = URLSession.shared
        task = URLSessionDownloadTask()
        self.cache = NSCache()
    }

    func obtainImageWithPath(imagePath: String, completionHandler: @escaping ImageCacheLoaderCompletionHandler) {
        if let image = self.cache.object(forKey: imagePath as NSString) {
            DispatchQueue.main.async {
                completionHandler(image)
            }
        } else {
            /* You need placeholder image in your assets, 
               if you want to display a placeholder to user */
            let placeholder = #imageLiteral(resourceName: "placeholder")
            DispatchQueue.main.async {
                completionHandler(placeholder)
            }
            let url: URL! = URL(string: imagePath)
            task = session.downloadTask(with: url, completionHandler: { (location, response, error) in
                if let data = try? Data(contentsOf: url) {
                    let img: UIImage! = UIImage(data: data)
                    self.cache.setObject(img, forKey: imagePath as NSString)
                    DispatchQueue.main.async {
                        completionHandler(img)
                    }
                }
            })
            task.resume()
        }
    }
}

使用例

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: "Identifier")

    cell.title = "Cool title"

    imageLoader.obtainImageWithPath(imagePath: viewModel.image) { (image) in
        // Before assigning the image, check whether the current cell is visible
        if let updateCell = tableView.cellForRow(at: indexPath) {
            updateCell.imageView.image = image
        }
    }    
    return cell
}
5

最良の答えは、これを行う正しい方法ではありません:(実際にはindex.Pathをmodelでバインドしています。イメージのロード中に行が追加されたことを想像してください。この状況は、ほとんどあり得ないため、再現するのは困難ですが、可能性もあります。

MVVMアプローチを使用し、コントローラのviewModelでセルをバインドし、viewModelでimageをロードし(switchToLatestメソッドでReactiveCocoaシグナルを割り当てる)、それからこのシグナルをサブスクライブしてimageをセルに割り当てるのが良いでしょう。 ;)

MVVMを悪用しないことを忘れないでください。見解は単純で死んでいる必要があります。 ViewModelは再利用可能なはずですが!そのため、View(UITableViewCell)とViewModelをコントローラにバインドすることが非常に重要です。

3
badeleux

これはSwiftのバージョンです(@Nitesh Borad Objective Cのコードを使用)。 -

   if let img: UIImage = UIImage(data: previewImg[indexPath.row]) {
                cell.cardPreview.image = img
            } else {
                // The image isn't cached, download the img data
                // We should perform this in a background thread
                let imgURL = NSURL(string: "webLink URL")
                let request: NSURLRequest = NSURLRequest(URL: imgURL!)
                let session = NSURLSession.sharedSession()
                let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
                    let error = error
                    let data = data
                    if error == nil {
                        // Convert the downloaded data in to a UIImage object
                        let image = UIImage(data: data!)
                        // Store the image in to our cache
                        self.previewImg[indexPath.row] = data!
                        // Update the cell
                        dispatch_async(dispatch_get_main_queue(), {
                            if let cell: YourTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as? YourTableViewCell {
                                cell.cardPreview.image = image
                            }
                        })
                    } else {
                        cell.cardPreview.image = UIImage(named: "defaultImage")
                    }
                })
                task.resume()
            }
3

私の場合は、画像のキャッシュによるものではありません(Used SDWebImage)。カスタムセルのタグがindexPath.rowと一致しないためです。

CellForRowAtIndexPathの場合:

1)カスタムセルにインデックス値を割り当てます。例えば、

cell.tag = indexPath.row

2)メインスレッドで、画像を割り当てる前に、画像をタグと照合して対応するセルに属するかどうかを確認します。

dispatch_async(dispatch_get_main_queue(), ^{
   if(cell.tag == indexPath.row) {
     UIImage *tmpImage = [[UIImage alloc] initWithData:imgData];
     thumbnailImageView.image = tmpImage;
   }});
});
3
A.G
 - (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;
    }
2
Dharmraj Vora

ありがとう "Rob" ....私はUICollectionViewと同じ問題を抱えていて、あなたの答えは私の問題を解決するのに役立ちます。これが私のコードです:

 if ([Dict valueForKey:@"ImageURL"] != [NSNull null])
    {
        cell.coverImageView.image = nil;
        cell.coverImageView.imageURL=nil;

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            if ([Dict valueForKey:@"ImageURL"] != [NSNull null] )
            {
                dispatch_async(dispatch_get_main_queue(), ^{

                    myCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];

                    if (updateCell)
                    {
                        cell.coverImageView.image = nil;
                        cell.coverImageView.imageURL=nil;

                        cell.coverImageView.imageURL=[NSURL URLWithString:[Dict valueForKey:@"ImageURL"]];

                    }
                    else
                    {
                        cell.coverImageView.image = nil;
                        cell.coverImageView.imageURL=nil;
                    }


                });
            }
        });

    }
    else
    {
        cell.coverImageView.image=[UIImage imageNamed:@"default_cover.png"];
    }
1
sneha

単純に変更

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];
     });
 });

Into

    dispatch_async(kBgQueue, ^{
         NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:   [NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];
         cell.poster.image = [UIImage imageWithData:imgData];
         dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
         });
     });
0

バックグラウンドでのセルのイメージロード時には、セルロードを高速化したいと思います。そのために、次の手順を実行しました。

  1. ファイルをチェックして、ドキュメントディレクトリに存在するかどうか。

  2. そうでなければ、それから初めてイメージをロードして、我々の電話文書ディレクトリにそれを保存します。画像を携帯電話に保存したくない場合は、セル画像を直接背景に読み込むことができます。

  3. 今ロードプロセス:

#import "ManabImageOperations.h"を含めるだけです

セルのコードは次のようになります。

NSString *imagestr=[NSString stringWithFormat:@"http://www.yourlink.com/%@",[dictn objectForKey:@"member_image"]];

        NSString *docDir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0];
        NSLog(@"Doc Dir: %@",docDir);

        NSString  *pngFilePath = [NSString stringWithFormat:@"%@/%@",docDir,[dictn objectForKey:@"member_image"]];

        BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:pngFilePath];
        if (fileExists)
        {
            [cell1.memberimage setImage:[UIImage imageWithContentsOfFile:pngFilePath] forState:UIControlStateNormal];
        }
        else
        {
            [ManabImageOperations processImageDataWithURLString:imagestr andBlock:^(NSData *imageData)
             {
                 [cell1.memberimage setImage:[[UIImage alloc]initWithData: imageData] forState:UIControlStateNormal];
                [imageData writeToFile:pngFilePath atomically:YES];
             }];
}

ManabImageOperations.h:

#import <Foundation/Foundation.h>

    @interface ManabImageOperations : NSObject
    {
    }
    + (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage;
    @end

ManabImageOperations.m:

#import "ManabImageOperations.h"
#import <QuartzCore/QuartzCore.h>
@implementation ManabImageOperations

+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage
{
    NSURL *url = [NSURL URLWithString:urlString];

    dispatch_queue_t callerQueue = dispatch_get_main_queue();
    dispatch_queue_t downloadQueue = dispatch_queue_create("com.myapp.processsmagequeue", NULL);
    dispatch_async(downloadQueue, ^{
        NSData * imageData = [NSData dataWithContentsOfURL:url];

        dispatch_async(callerQueue, ^{
            processImage(imageData);
        });
    });
  //  downloadQueue=nil;
    dispatch_release(downloadQueue);

}
@end

問題が発生した場合は、回答とコメントを確認してください。

0
Manab Kumar Mal

URLを渡すだけで

NSURL *url = [NSURL URLWithString:@"http://www.myurl.com/1.png"];
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(), ^{
                    yourimageview.image = image;
            });
        }
    }
}];
[task resume];
0
User558