web-dev-qa-db-ja.com

GCDを使用したUITableViewの画像の非同期ダウンロード

GCDを使用してuitableviewの画像を非同期にダウンロードしていますが、問題があります-画像をスクロールすると、常にちらつき、変化します。私はすべてのセルでイメージをゼロに設定しようとしましたが、それはあまり役に立ちません。速くスクロールバックすると、すべての画像が間違っています。私はそれについて何ができますか?ここに私の細胞の方法があります:

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

    if (self.loader.parsedData[indexPath.row] != nil)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
            dispatch_async(queue, ^(void) {

                NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]];

                UIImage* image = [[UIImage alloc] initWithData:imageData];

                dispatch_async(dispatch_get_main_queue(), ^{
                    cell.imageView.image = image;
                    [cell setNeedsLayout];
                     });
            });

    cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"];
    }
    return cell;
}
34
Dvole

ここでの問題は、画像取得ブロックがTableViewセルへの参照を保持していることです。ダウンロードが完了すると、imageView.imageプロパティ(セルをリサイクルして別の行を表示した場合でも)。

画像を設定する前に、画像がまだセルに関連しているかどうかをテストするには、ダウンロード完了ブロックが必要になります。

また、セル以外の場所に画像を保存していないため、画面上で行をスクロールするたびに画像を再度ダウンロードすることに注意してください。ダウンロードを開始する前に、おそらくどこかにキャッシュして、ローカルにキャッシュされた画像を探したいでしょう。

編集:セルのtagプロパティを使用して、テストする簡単な方法を次に示します。

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

    cell.tag = indexPath.row;
    NSDictionary *parsedData = self.loader.parsedData[indexPath.row];
    if (parsedData)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
        dispatch_async(queue, ^(void) {

            NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:parsedData[@"imageLR"]];

            UIImage* image = [[UIImage alloc] initWithData:imageData];
            if (image) {
                 dispatch_async(dispatch_get_main_queue(), ^{
                     if (cell.tag == indexPath.row) {
                         cell.imageView.image = image;
                         [cell setNeedsLayout];
                     }
                 });
             }
        });

        cell.textLabel.text = parsedData[@"id"];
    }
    return cell;
}
95
Seamus Campbell

ポイントは、セルの再利用の概念を完全に理解していなかったことです。これは、非同期ダウンロードとはあまりよく一致しません。

ブロック

    ^{
    cell.imageView.image = image;
    [cell setNeedsLayout];
}

リクエストが終了し、すべてのデータがロードされたときに実行されます。しかし、セルはブロックが作成されるときにその値を取得します。

ブロックが実行されるまでに、セルはまだ既存のセルのいずれかを指します。しかし、ユーザーがスクロールを続けた可能性は非常に高いです。セルオブジェクトはその間に再利用され、イメージは再利用され、割り当てられて表示される 'old'セルに関連付けられます。その後まもなく、ユーザーがさらにスクロールしない限り、正しい画像がロードされ、割り当てられて表示されます。などなど。

よりスマートな方法を探してください。たくさんのチュロリアルがあります。遅延イメージの読み込みのためのGoogle。

7
Hermann Klecker

インデックスパスを使用してセルを取得します。表示されていない場合、セルはnilになり、問題は発生しません。もちろん、ダウンロードしたデータをキャッシュして、画像が既にある場合にセルの画像をすぐに設定することをお勧めします。

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

    if (self.loader.parsedData[indexPath.row] != nil)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
            dispatch_async(queue, ^(void) {
                //  You may want to cache this explicitly instead of reloading every time.
                NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]];
                UIImage* image = [[UIImage alloc] initWithData:imageData];
                dispatch_async(dispatch_get_main_queue(), ^{
                    // Capture the indexPath variable, not the cell variable, and use that
                    UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath];
                    blockCell.imageView.image = image;
                    [blockCell setNeedsLayout];
                });
            });
        cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"];
    }

    return cell;
}
6
Carl Veazey

私はこの問題について研究してきましたが、UITableViewCellをカスタマイズすることで優れたアプローチを見つけました。

#import <UIKit/UIKit.h>

@interface MyCustomCell : UITableViewCell

@property (nonatomic, strong) NSURLSessionDataTask *imageDownloadTask;
@property (nonatomic, weak) IBOutlet UIImageView *myImageView;
@property (nonatomic, weak) IBOutlet UIActivityIndicatorView *activityIndicator;

@end

次に、TableViewControllerで、NSURLSessionConfigurationとNSURLSessionの2つのプロパティを宣言し、ViewDidLoadで初期化します。

@interface MyTableViewController ()

@property (nonatomic, strong) NSURLSessionConfiguration *sessionConfig;
@property (nonatomic, strong) NSURLSession *session;
.
.
.
@end

@implementation TimesVC

- (void)viewDidLoad
{
    [super viewDidLoad];

    _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    _session = [NSURLSession sessionWithConfiguration:_sessionConfig];
}

.
.
.

データソースがNSMutableDictionaryの配列(またはNSManagedObjectContext)であると仮定しましょう。次のようなキャッシングを使用して、各セルのイメージを簡単にダウンロードできます。

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

    if (!cell)
    {
        cell = [[MyCustomCell alloc] initWithStyle:UITableViewCellStyleDefault
                                reuseIdentifier:@"cell"];
    }

    NSMutableDictionary *myDictionary = [_myArrayDataSource objectAtIndex:indexPath.row];    

    if (cell.imageDownloadTask)
    {
        [cell.imageDownloadTask cancel];
    }

    [cell.activityIndicator startAnimating];
    cell.myImageView.image = nil;

    if (![myDictionary valueForKey:@"image"])
    {
        NSString *urlString = [myDictionary valueForKey:@"imageURL"];
        NSURL *imageURL = [NSURL URLWithString:urlString];
        if (imageURL)
        {
            cell.imageDownloadTask = [_session dataTaskWithURL:imageURL
                completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
            {
                if (error)
                {
                    NSLog(@"ERROR: %@", error);
                }
                else
                {
                    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

                    if (httpResponse.statusCode == 200)
                    {
                        UIImage *image = [UIImage imageWithData:data];

                        dispatch_async(dispatch_get_main_queue(), ^{
                            [myDictionary setValue:data forKey:@"image"];
                            [cell.myImageView setImage:image];
                            [cell.activityIndicator stopAnimating];
                        });
                    }
                    else
                    {
                        NSLog(@"Couldn't load image at URL: %@", imageURL);
                        NSLog(@"HTTP %d", httpResponse.statusCode);
                    }
                }
            }];

            [cell.imageDownloadTask resume];
        }
    }
    else
    {
        [cell.myImageView setImage:[UIImage imageWithData:[myDictionary valueForKey:@"image"]]];
        [cell.activityIndicator stopAnimating];
    }

    return cell;
}

一部の開発者に役立つことを願っています!乾杯。

クレジット: iOS 7のテーブルビュー画像

3
Tom Calmon

車輪を再発明するのではなく、次のすばらしいポッドを使用してください。

https://github.com/rs/SDWebImage

https://github.com/JJSaccolo/UIActivityIndi​​cator-for-SDWebImage

単純な:

    [self.eventImage setImageWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", [SchemeConfiguration APIEndpoint] , _event.imageUrl]]
                          placeholderImage:nil
                                 completed:^(UIImage *image,
                                             NSError *error,
                                             SDImageCacheType cacheType,
                                             NSURL *imageURL)
     {
         event.image = UIImagePNGRepresentation(image);
     } usingActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
1
halbano

SDWebImageの使用を考えたことがありますか?また、指定されたURLから非同期に画像をダウンロードします。ライブラリ全体を使用しませんでした(UIImageView + WebCache.hのみをインポートしました)。インポートしたら、次のようにメソッドを呼び出すだけです。

[UIImageXYZ sd_setImageWithURL:["url you're retrieving from"] placeholderImage:[UIImage imageNamed:@"defaultProfile.jpeg"]];

AFNetworking 2.0を使用している場合はやり過ぎかもしれませんが、うまくいきました。

試してみたい場合は、githubへのリンクをご覧ください

1
RockyEEKlvn

Seamus Campbellの受け入れられた答えに加えて、いつかこれが機能しないことも知っておく必要があります。その場合、特定のセルをリロードする必要があります。そう

if (image) {
     dispatch_async(dispatch_get_main_queue(), ^{
          if (cell.tag == indexPath.row) {
               cell.imageView.image = image;
               [cell setNeedsLayout];
          }
      });
 }

に変更する必要があります

    if (image) {
         dispatch_async(dispatch_get_main_queue(), ^{
              if (cell.tag == indexPath.row) {
                   cell.imageView.image = image;
                   self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
              }
          });
     }
1

インデックスパスの変更と一貫性の欠如を心配する場合、潜在的な解決策は、UITableViewCellまたはUICollectionViewCellをサブクラス化し、stringTagなどのインスタンス変数を追加することです。次に、ダウンロードする写真のURLをstringTagに入れます。画像を設定するとき、stringTagが依然として正しいURLであるかどうかを確認します。

詳細については、この回答を参照してください: 非同期フェッチが完了しました:セルがまだ表示されていますか?

私のクラスは次のとおりです。

import Foundation
import UIKit

class ImageAsyncCollectionViewCell : UICollectionViewCell {
  var stringTag: String?
}

そして、セルを使用する場合:

    cell.stringTag = photoKey
    cell.imageView.image = self.blankImage
    if ImageCache.default.imageCachedType(forKey: photoKey).cached {
      ImageCache.default.retrieveImage(forKey: photoKey, options: nil) {
        image, cacheType in
        if let image = image {
          DispatchQueue.main.async {
            if cell.stringTag == photoKey {
              cell.imageView.image = image
            }
          }
        } else {
          print("Doesn't exist in cache, but should")
          self.setCellWithPhoto(photoKey: photoKey, cell: cell)
        }
      }
    } else {
      self.setCellWithPhoto(photoKey: photoKey, cell: cell)
    }
0
Ronak Vora