it-swarm.com.ru

Асинхронная загрузка изображения из URL-адреса внутри ячейки UITableView - при прокрутке изображение изменяется на неправильное

Я написал два способа асинхронной загрузки изображений внутри моей ячейки UITableView. В обоих случаях изображение будет загружаться нормально, но когда я прокручиваю таблицу, изображения будут меняться несколько раз, пока прокрутка не закончится и изображение не вернется к нужному изображению. Я понятия не имею, почему это происходит.

#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;
}
146
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. Более тонкая проблема заключается в том, что в действительно медленной сети ваш асинхронный запрос может не завершиться, пока ячейка не прокрутится с экрана. Вы можете использовать метод UITableViewcellForRowAtIndexPath: (не путать с одноименным методом UITableViewDataSourcetableView:cellForRowAtIndexPath:), чтобы увидеть, видна ли еще ячейка для этой строки. Этот метод вернет nil, если ячейка не видна.

    Проблема заключается в том, что ячейка прокручивалась к тому времени, когда ваш асинхронный метод завершился, и, что еще хуже, ячейка была повторно использована для другой строки таблицы. Проверяя, видна ли еще строка, вы убедитесь, что случайно не обновили изображение изображением для строки, которая с тех пор прокрутилась за пределы экрана.

  3. В некоторой степени не связанный с рассматриваемым вопросом, я все еще был вынужден обновить его, чтобы использовать современные соглашения и API, в частности:

    • Используйте NSURLSession вместо отправки -[NSData contentsOfURL:] в фоновую очередь;

    • Используйте dequeueReusableCellWithIdentifier:forIndexPath: вместо dequeueReusableCellWithIdentifier: (но обязательно используйте прототип ячейки или класс регистра или NIB для этого идентификатора); а также

    • Я использовал имя класса, которое соответствует соглашениям об именах Какао (т.е. начинаются с заглавной буквы).

Даже с этими исправлениями есть проблемы:

  1. Приведенный выше код не кэширует загруженные изображения. Это означает, что если вы прокрутите изображение за пределы экрана и вернетесь на экран, приложение может попытаться получить изображение снова. Возможно, вам повезет, что ваши заголовки ответа сервера позволят довольно прозрачное кэширование, предлагаемое NSURLSession и NSURLCache, но если нет, вы будете делать ненужные запросы к серверу и предлагать гораздо более медленный UX.

  2. Мы не отменяем запросы на ячейки, которые прокручиваются за пределы экрана. Таким образом, если вы быстро прокрутите до 100-й строки, изображение для этой строки может быть задержано позади запросов на предыдущие 99 строк, которые даже больше не видны. Вы всегда хотите убедиться, что вы расставляете приоритеты запросов на видимые ячейки для лучшего UX.

Самое простое решение, которое решает эти проблемы, - это использование категории UIImageView, например, предоставляемой с SDWebImage или AFNetworking . Если вы хотите, вы можете написать свой собственный код для решения вышеуказанных проблем, но это большая работа, и вышеупомянутые категории UIImageView уже сделали это для вас.

215
Rob

/ * Я сделал это таким образом, а также проверил это * /

Шаг 1 = Зарегистрировать пользовательский класс ячейки (в случае ячейки прототипа в таблице) или nib (в случае настраиваемого пера для настраиваемой ячейки) для таблицы, как это в методе viewDidLoad:

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

OR

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

Шаг 2 = Используйте метод UITableView "dequeueReusableCellWithIdentifier: forIndexPath:", как этот (для этого вы должны зарегистрировать класс или nib):

   - (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

Есть несколько платформ, которые решают эту проблему. Просто назвать несколько:

Swift:

Objective-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
}
4
Dmitrii Klassneckii

Лучший ответ - не правильный способ сделать это :(. Вы на самом деле связали indexPath с моделью, что не всегда хорошо. Представьте, что некоторые строки были добавлены во время загрузки изображения. Теперь ячейка для данного indexPath существует на экране, но изображение больше не правильно! Ситуация вроде маловероятна и ее трудно воспроизвести, но это возможно.

Лучше использовать подход MVVM, связать ячейку с viewModel в контроллере и загрузить изображение в viewModel (присвоение сигнала ReactiveCocoa методом switchToLatest), затем подписать этот сигнал и назначить изображение ячейке! ;)

Вы должны помнить, чтобы не злоупотреблять MVVM. Взгляды должны быть до смерти просты! Принимая во внимание, что ViewModels должен быть многоразовым! Вот почему очень важно связать 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
Chathuranga Silva

В моем случае это не связано с кэшированием изображений (используется 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

Спасибо "Роб" .... У меня была такая же проблема с 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];
     });
 });

В

    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
Sazzad Hissain Khan

Я думаю, что вы хотите ускорить загрузку вашей ячейки во время загрузки изображения для ячейки в фоновом режиме. Для этого мы сделали следующие шаги:

  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