2013-03-29 11 views
12

Ich verwende SDWebImage-Bibliothek, um Remote-Images in eine Tabellenansicht zu laden, die eine benutzerdefinierte Zellklasse verwendet, die ich erstellt habe. Ich habe einfachSDWebImage lädt keine Remote-Images, bis scroll

[cell.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:@"loading.jpg"]]; 

in cellForRowAtIndexPath verwenden: Nun das Problem ist es Bilder in den sichtbaren Zellen und nicht für die Zellen lädt, die für die i scrollen ist offscreen nach oben und unten, sie laden zu machen. Gibt es eine Möglichkeit, alle Bilder zu laden, ohne die Tabelle zu scrollen. Vielen Dank im Voraus !!

+0

Sind Sie sicher, dass diese Zellen schaffen, die offscreen sind? Kennen Sie das Verhalten von UITableView? – Exploring

+0

Die Zellen werden wiederverwendet. – user2082760

+0

Ja, welche Zellen sind dann offline, dann wird die Instanzmethode für diese Zellen aufgerufen? Es ist nicht das Problem von SDWebImage. – Exploring

Antwort

20

Wenn Sie Zeilen vorab abrufen möchten, können Sie auf die Methoden UIScrollViewDelegate reagieren, um zu bestimmen, wann das Tabellenscrolling ausgeführt wird, wodurch ein Prefetch der Zeilen ausgelöst wird. Sie können die Prefetch mit SDWebImagePrefetcher durchführen (in meiner ursprünglichen Antwort, die ich ein wenig abweisend dieser nützlichen Klasse war, aber es scheint relativ gut an die Arbeit):

- (void)viewDidLoad 
{ 
    [super viewDidLoad]; 

    // the details don't really matter here, but the idea is to fetch data, 
    // call `reloadData`, and then prefetch the other images 

    NSURL *url = [NSURL URLWithString:kUrlWithJSONData]; 
    NSURLRequest *request = [NSURLRequest requestWithURL:url]; 
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { 
     if (connectionError) { 
      NSLog(@"sendAsynchronousRequest error: %@", connectionError); 
      return; 
     } 

     self.objects = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; 

     [self.tableView reloadData]; 

     [self prefetchImagesForTableView:self.tableView]; 
    }]; 
} 

// some of the basic `UITableViewDataDelegate` methods have been omitted because they're not really relevant 

Hier ist die einfache cellForRowAtIndexPath (nicht ganz relevant, sondern nur zeigen, dass, wenn Sie SDWebImagePrefetcher verwenden, müssen Sie nicht zu verwirren haben um mit cellForRowAtIndexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{ 
    static NSString *cellIdentifier = @"Cell"; 
    CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; 
    NSAssert([cell isKindOfClass:[CustomCell class]], @"cell should be CustomCell"); 

    [cell.customImageView setImageWithURL:[self urlForIndexPath:indexPath] placeholderImage:nil]; 
    [cell.customLabel setText:[self textForIndexPath:indexPath]]; 

    return cell; 
} 

Diese UIScrollViewDelegate Methoden prefetch mehr Zeilen, wenn Oberflächen

Scrollen
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView 
{ 
    // if `decelerate` was true for `scrollViewDidEndDragging:willDecelerate:` 
    // this will be called when the deceleration is done 

    [self prefetchImagesForTableView:self.tableView]; 
} 

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate 
{ 
    // if `decelerate` is true, then we shouldn't start prefetching yet, because 
    // `cellForRowAtIndexPath` will be hard at work returning cells for the currently visible 
    // cells. 

    if (!decelerate) 
     [self prefetchImagesForTableView:self.tableView]; 
} 

Sie müssen natürlich eine Vorabrufroutine implementieren. Dies ruft die NSIndexPath Werte für die Zellen auf jeder Seite der sichtbaren Zellen ab, ruft ihre Bild-URLs ab und ruft dann diese Daten vorab ab.

/** Prefetch a certain number of images for rows prior to and subsequent to the currently visible cells 
* 
* @param tableView The tableview for which we're going to prefetch images. 
*/ 

- (void)prefetchImagesForTableView:(UITableView *)tableView 
{ 
    NSArray *indexPaths = [self.tableView indexPathsForVisibleRows]; 
    if ([indexPaths count] == 0) return; 

    NSIndexPath *minimumIndexPath = indexPaths[0]; 
    NSIndexPath *maximumIndexPath = [indexPaths lastObject]; 

    // they should be sorted already, but if not, update min and max accordingly 

    for (NSIndexPath *indexPath in indexPaths) 
    { 
     if (indexPath.section < minimumIndexPath.section || (indexPath.section == minimumIndexPath.section && indexPath.row < minimumIndexPath.row)) minimumIndexPath = indexPath; 
     if (indexPath.section > maximumIndexPath.section || (indexPath.section == maximumIndexPath.section && indexPath.row > maximumIndexPath.row)) maximumIndexPath = indexPath; 
    } 

    // build array of imageURLs for cells to prefetch 

    NSMutableArray *imageURLs = [NSMutableArray array]; 
    indexPaths = [self tableView:tableView priorIndexPathCount:kPrefetchRowCount fromIndexPath:minimumIndexPath]; 
    for (NSIndexPath *indexPath in indexPaths) 
     [imageURLs addObject:[self urlForIndexPath:indexPath]]; 
    indexPaths = [self tableView:tableView nextIndexPathCount:kPrefetchRowCount fromIndexPath:maximumIndexPath]; 
    for (NSIndexPath *indexPath in indexPaths) 
     [imageURLs addObject:[self urlForIndexPath:indexPath]]; 

    // now prefetch 

    if ([imageURLs count] > 0) 
    { 
     [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:imageURLs]; 
    } 
} 

Dies sind die Utility-Methoden für das Erhalten der NSIndexPath für die Zeilen sofort die sichtbaren Zellen vorhergehenden sowie diese unmittelbar nach den sichtbaren Zellen:

/** Retrieve NSIndexPath for a certain number of rows preceding particular NSIndexPath in the table view. 
* 
* @param tableView The tableview for which we're going to retrieve indexPaths. 
* @param count  The number of rows to retrieve 
* @param indexPath The indexPath where we're going to start (presumably the first visible indexPath) 
* 
* @return   An array of indexPaths. 
*/ 

- (NSArray *)tableView:(UITableView *)tableView priorIndexPathCount:(NSInteger)count fromIndexPath:(NSIndexPath *)indexPath 
{ 
    NSMutableArray *indexPaths = [NSMutableArray array]; 
    NSInteger row = indexPath.row; 
    NSInteger section = indexPath.section; 

    for (NSInteger i = 0; i < count; i++) { 
     if (row == 0) { 
      if (section == 0) { 
       return indexPaths; 
      } else { 
       section--; 
       row = [tableView numberOfRowsInSection:section] - 1; 
      } 
     } else { 
      row--; 
     } 
     [indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:section]]; 
    } 

    return indexPaths; 
} 

/** Retrieve NSIndexPath for a certain number of following particular NSIndexPath in the table view. 
* 
* @param tableView The tableview for which we're going to retrieve indexPaths. 
* @param count  The number of rows to retrieve 
* @param indexPath The indexPath where we're going to start (presumably the last visible indexPath) 
* 
* @return   An array of indexPaths. 
*/ 

- (NSArray *)tableView:(UITableView *)tableView nextIndexPathCount:(NSInteger)count fromIndexPath:(NSIndexPath *)indexPath 
{ 
    NSMutableArray *indexPaths = [NSMutableArray array]; 
    NSInteger row = indexPath.row; 
    NSInteger section = indexPath.section; 
    NSInteger rowCountForSection = [tableView numberOfRowsInSection:section]; 

    for (NSInteger i = 0; i < count; i++) { 
     row++; 
     if (row == rowCountForSection) { 
      row = 0; 
      section++; 
      if (section == [tableView numberOfSections]) { 
       return indexPaths; 
      } 
      rowCountForSection = [tableView numberOfRowsInSection:section]; 
     } 
     [indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:section]]; 
    } 

    return indexPaths; 
} 

Es gibt es eine Menge, aber in Wirklichkeit, SDWebImage und seine SDWebImagePrefetcher macht das schwere Heben.

Ich füge meine ursprüngliche Antwort aus Gründen der Vollständigkeit hier ein.


Ursprüngliche Antwort:

Wenn Sie etwas Prefetching mit SDWebImage tun mögen, könnten Sie so etwas wie die folgenden tun:

  1. einen Abschluss Block zu Ihrem setImageWithURL Anruf hinzufügen:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
    { 
        NSLog(@"%s", __FUNCTION__); 
    
        static NSString *cellIdentifier = @"Cell"; 
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; 
    
        TableModelRow *rowData = self.objects[indexPath.row]; 
    
        cell.textLabel.text = rowData.title; 
        [cell.imageView setImageWithURL:rowData.url 
            placeholderImage:[UIImage imageNamed:@"placeholder.png"] 
              completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) { 
               [self prefetchImagesForTableView:tableView]; 
              }]; 
    
        return cell; 
    } 
    

    Ich muss gestehen, ich mag nicht wirklich anrufen meine prefetcher Routine hier (ich wünschte, iOS hätte einige nette didFinishTableRefresh Delegate-Methode), aber es funktioniert, auch wenn es die Routine öfter anruft, als ich wirklich wollen würde. Ich stelle nur sicher, dass die Routine unten sicherstellt, dass keine redundanten Anfragen gestellt werden.

  2. Wie dem auch sei, ich schreibe eine Prefetch-Routine, die für, sagen wir, in den nächsten zehn Bilder aussieht:

    const NSInteger kPrefetchRowCount = 10; 
    
    - (void)prefetchImagesForTableView:(UITableView *)tableView 
    { 
        // determine the minimum and maximum visible rows 
    
        NSArray *indexPathsForVisibleRows = [tableView indexPathsForVisibleRows]; 
        NSInteger minimumVisibleRow = [indexPathsForVisibleRows[0] row]; 
        NSInteger maximumVisibleRow = [indexPathsForVisibleRows[0] row]; 
    
        for (NSIndexPath *indexPath in indexPathsForVisibleRows) 
        { 
         if (indexPath.row < minimumVisibleRow) minimumVisibleRow = indexPath.row; 
         if (indexPath.row > maximumVisibleRow) maximumVisibleRow = indexPath.row; 
        } 
    
        // now iterate through our model; 
        // `self.objects` is an array of `TableModelRow` objects, one object 
        // for every row of the table. 
    
        [self.objects enumerateObjectsUsingBlock:^(TableModelRow *obj, NSUInteger idx, BOOL *stop) { 
         NSAssert([obj isKindOfClass:[TableModelRow class]], @"Expected TableModelRow object"); 
    
         // if the index is within `kPrefetchRowCount` rows of our visible rows, let's 
         // fetch the image, if it hasn't already done so. 
    
         if ((idx < minimumVisibleRow && idx >= (minimumVisibleRow - kPrefetchRowCount)) || 
          (idx > maximumVisibleRow && idx <= (maximumVisibleRow + kPrefetchRowCount))) 
         { 
          // my model object has method for initiating a download if needed 
    
          [obj downloadImageIfNeeded]; 
         } 
        }]; 
    } 
    
  3. In der Download-Routine, können Sie überprüfen, ob der Download Bild hat begonnen, und wenn nicht, dann starte es. Um dies zu tun mit SDWebImage, halte ich einen weak Zeiger auf den Web-Bild Betrieb in meiner TableModelRow Klasse (die Modellklasse, die die einzelnen Reihen von meinem Tisch Rücken):

    @property (nonatomic, weak) id<SDWebImageOperation> webImageOperation; 
    

    Ich habe dann den downloadImageIfNeeded Routine beginnt einen Download wenn es nicht schon ist (Sie können sehen, warum die Erstellung dieser weak so wichtig war ... Ich überprüfe, ob in dieser Zeile bereits ein Vorgang aussteht, bevor ein anderer gestartet wird). Ich mache nichts mit dem heruntergeladenen Bild (kurz, für Debugging-Zwecke, Protokollierung der Tatsache, dass ein Download durchgeführt wurde), sondern einfach nur herunterladen und lassen SDImageWeb verfolgen das zwischengespeicherte Bild für mich, also wenn cellForRowAtIndexPath später anfordert Bild, wenn der Benutzer scrollt, ist es da, bereit und wartet.

    - (void)downloadImageIfNeeded 
    { 
        if (self.webImageOperation) 
         return; 
    
        SDWebImageManager *imageManager = [SDWebImageManager sharedManager]; 
    
        self.webImageOperation = [imageManager downloadWithURL:self.url 
                    options:0 
                    progress:nil 
                   completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) { 
                    NSLog(@"%s: downloaded %@", __FUNCTION__, self.title); 
                    // I'm not going to do anything with the image, but `SDWebImage` has now cached it for me 
                   }]; 
    } 
    

    Ein Teil von mir denkt, es könnte robuster sein imageManager.imageCache Instanz Methode aufrufen queryDiskCacheForKey zuerst, aber nach ein paar Tests zu tun, es sieht nicht so aus, dass benötigt wird (und die downloadWithURL macht das für uns jedenfalls).

Ich möchte darauf hinweisen, dass die SDImageWeb Bibliothek hat eine SDWebImagePrefetcher Klasse (siehe the documentation). Der Name der Klasse ist unglaublich vielversprechend, aber wenn ich mir den Code mit aller Ehrerbietung gegenüber einer ansonsten exzellenten Bibliothek anschaue, fühlt sich das für mich nicht sehr robust an (zB ist es eine einfache Liste von URLs, die man holen kann und wenn man es nochmal macht) , löscht es die vorherige Liste ohne eine Vorstellung von "Hinzufügen zu der Warteschlange" oder ähnlichem. Es ist eine vielversprechende Vorstellung, aber ein wenig schwach in der Ausführung. Und als ich es versuchte, litt meine UX merklich.

Also, ich bin geneigt, SDWebImagePrefetcher (bis es zumindest verbessert wird) nicht zu verwenden, und bleiben Sie bei meiner rudimentären Prefetch-Technik. Es ist nicht besonders anspruchsvoll, aber es scheint zu funktionieren.

+0

wow..das war fantastisch Rob..thanx eine Tonne !! – user2082760

+1

Danke für diese großartige Antwort. Ich schlage eine leichte Verbesserung dieser Antwort vor, wie Apple in ihrem Beispiel Lazy Loading durchführt. [link] (https://developer.apple.com/library/ios/samplecode/LazyTableImages/Introduction/Intro.html). Warum nicht verwenden '- (void) scrollViewDidEndDragging: (UIScrollView *) scrollView wird verzögert: (BOOL) bremse' und '- (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView ' –

+0

@AnilPuttabuddhi Vereinbar, können Sie dies mit diesen 'UIScrollViewDelegate auslösen 'Methoden. Ich habe meine Antwort mit einer Version aktualisiert, die dies nicht nur tut, sondern auch "SDWebImagePrefetcher" verwendet. Ich glaube, dass diese Wiedergabe auch mehrere Abschnitte besser behandelt. – Rob

1

Dies ist ein Beispiel und Sie müssen dies für Ihren Zweck implementieren.
Ihre UITableView Delegierten:

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

    if (!cell) 
    { 
     cell = [[[YourCustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault 
           reuseIdentifier:CellIdentifier];   
    } 

    NSString *imageURL = // ... get image url, typically from array 
    [cell loadImageWithURLString:imageURL forIndexPath:indexPath]; 

    return cell; 
} 

Ihre benutzerdefinierte UITableViewCell .h-Datei:

#import <UIKit/UIKit.h> 
#import "UIImageView+WebCache.h" 
#import "SDImageCache.h" 

@interface YourCustomTableViewCell 
{ 
    NSIndexPath *currentLoadingIndexPath; 
} 

- (void)loadImageWithURLString:(NSString *)urlString forIndexPath:(NSIndexPath *)indexPath; 

@end 

Ihre benutzerdefinierte UITableViewCell .m-Datei:

// ... some other methods 

- (void)loadImageWithURLString:(NSString *)urlString forIndexPath:(NSIndexPath *)indexPath 
{ 
    currentLoadingIndexPath = indexPath; 
    [self.imageView cancelCurrentImageLoad]; 
    [self.imageView setImage:nil]; 

    NSURL *imageURL = [NSURL URLWithString:urlString]; 
    [self.imageView setImageWithURL:imageURL 
        placeholderImage:nil 
          options:SDWebImageRetryFailed 
          completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) 
    { 
     if (currentLoadingIndexPath != indexPath) 
     { 
      return; 
     } 

     if (error) 
     { 
      ... // handle error 
     } 
     else 
     { 
      [imageView setImage:image]; 
     } 
    }]; 
} 

// ... some other methods 

currentLoadingIndexPath erforderlich, um festzustellen, ob wir diese Zelle für ein anderes Bild anstelle eines Bildes verwenden, das heruntergeladen wurde, während der Benutzer die Tabellenansicht scrollt.

2

Ich musste nur genau dieses Problem lösen und wollte nicht den Overhead des Prefetcher. Es muss ein paar zusätzliche Dinge unter der Oberfläche passieren, die mit der eingebauten imageView -Eigenschaft passieren, die das Laden verhindert, weil eine neue UIImageView gut funktioniert.

Meine Lösung ist ziemlich sauber, wenn Sie nichts dagegen haben, (oder bereits) unter Verwendung einer Unterklasse von UITableViewCell:

  1. Subclass UITableViewCell.
  2. Verbergen Sie in Ihrer Unterklasse self.imageView.
  3. Erstellen Sie Ihre eigene UIImageView Unteransicht und setzen Sie dieses Bild der Ansicht.

Hier ist eine modifizierte Version von meinem eigenen Code (undokumentiert hier ist die Rahmeneinstellung umfasst die Größe & Position des iOS Photos App Albums angepasst):

YourTableCell.h

@interface YourTableCell : UITableViewCell 
    @property (nonatomic, strong) UIImageView *coverPhoto; 
@end 

YourTableCell.m

@implementation YourTableCell 

@synthesize coverPhoto; 

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 
{ 
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 
    if (self) { 
     self.imageView.image = nil; 
     self.coverPhoto = [[UIImageView alloc] init]; 

     // Any customization, such as initial image, frame bounds, etc. goes here.   

     [self.contentView addSubview:self.coverPhoto]; 
    } 
    return self; 
} 
//... 
@end 

YourTableViewController.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{ 
    static NSString *CellIdentifier = @"Cell"; 
    YourTableCell *cell = (YourTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 
    //... 
    [cell.coverPhoto setImageWithURL:coverUrl placeholderImage:nil options:SDWebImageCacheMemoryOnly]; 
    //... 
} 
0

ich das gleiche Problem traf, fand ich UIImageView + WebCache letzten Download abzubrechen, wenn ein neuer Download kommen.

Ich bin nicht sicher, ob dies die Absicht des Autors ist. Also schreibe ich eine neue category von UIImageView Base auf SDWebImage.

Einfach zu bedienen:

[cell.imageView mq_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] 
        groupIdentifier:@"customGroupID" 
         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { 

         }]; 

mehr an: ImageDownloadGroup

Erweiterte Nutzung:

// create customGroup 
MQImageDownloadGroup *customGroup = [[MQImageDownloadGroup alloc] initWithGroupIdentifier:@"tableViewCellGroup"]; 
customGroup.maxConcurrentDownloads = 99; 

// add to MQImageDownloadGroupManage 
[[MQImageDownloadGroupManage shareInstance] addGroup:customGroup]; 

// use download group 
[cell.imageView mq_setImageWithURL:@"https://xxx" 
        groupIdentifier:@"tableViewCellGroup" 
         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { 

         }];