[ios] UITableView 셀 내부의 URL에서 비동기 이미지로드-스크롤하는 동안 이미지가 잘못된 이미지로 변경됨

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



답변

빠른 전술 수정을 찾고 있다고 가정하면 셀 이미지가 초기화되고 셀 행이 여전히 표시되는지 확인해야합니다.

- (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. 백그라운드 요청을 시작하기 전에 셀 이미지를 초기화하지 않습니다 (새 이미지를 다운로드하는 동안 대기열에서 제외 된 셀의 마지막 이미지가 계속 표시됨). 이미지보기 nilimage속성을 확인하십시오. 그렇지 않으면 이미지가 깜박입니다.

  2. 더 미묘한 문제는 실제로 느린 네트워크에서는 셀이 화면을 스크롤하기 전에 비동기 요청이 완료되지 않을 수 있다는 것입니다. 당신은 사용할 수 있습니다UITableView메소드cellForRowAtIndexPath: (유사하게 이름이 지정된 UITableViewDataSourcemethod 와 혼동하지 말 것 tableView:cellForRowAtIndexPath:) 해당 행의 셀이 여전히 표시되는지 확인할 수 있습니다. nil셀이 보이지 않으면 이 메소드가 반환 됩니다.

    문제는 비동기 메서드가 완료 될 때까지 셀이 스크롤되어 테이블의 다른 행에 다시 사용되었다는 것입니다. 행이 여전히 보이는지 확인하여 화면에서 스크롤 된 행의 이미지로 실수로 이미지를 업데이트하지 않도록합니다.

  3. 당면한 질문과 관련이 없지만, 나는 여전히 현대의 규칙과 API를 활용하기 위해 이것을 업데이트해야한다고 느꼈습니다.

    • 사용하다 NSURLSession-[NSData contentsOfURL:]백그라운드 큐로 디스패치하지 않고 .

    • dequeueReusableCellWithIdentifier:forIndexPath:오히려 사용dequeueReusableCellWithIdentifier: (그러나 해당 프로토 타입에 셀 프로토 타입 또는 레지스터 클래스 또는 NIB를 사용해야합니다). 과

    • Cocoa 명명 규칙 을 준수하는 클래스 이름을 사용했습니다 (예 : 대문자로 시작).

이러한 수정에도 문제가 있습니다.

  1. 위의 코드는 다운로드 한 이미지를 캐싱하지 않습니다. 즉, 이미지를 화면 밖으로 스크롤하고 다시 화면에서 다시 스크롤하면 앱이 이미지를 다시 검색하려고 할 수 있습니다. 아마도 서버 응답 헤더가 NSURLSessionNSURLCache에서 제공하는 상당히 투명한 캐싱을 허용 할 정도로 운이 좋을 수도 있지만 그렇지 않은 경우 불필요한 서버 요청을하고 훨씬 느린 UX를 제공 할 것입니다.

  2. 화면을 스크롤하는 셀에 대한 요청을 취소하지 않습니다. 따라서 100 번째 행으로 빠르게 스크롤하면 해당 행의 이미지가 더 이상 표시되지 않는 이전 99 개의 행에 대한 요청 뒤에 백 로그 될 수 있습니다. 항상 최상의 UX에 대한 가시적 셀에 대한 요청의 우선 순위를 지정하려고합니다.

이러한 문제를 해결하는 가장 간단한 해결 방법 UIImageViewSDWebImage 또는 AFNetworking 과 같은 범주 를 사용하는 입니다. 원하는 경우 위의 문제를 처리하기 위해 자체 코드를 작성할 수 있지만 많은 작업이 있으며 위의 UIImageView범주는 이미이 작업을 수행했습니다.


답변

/ * 나는이 방법으로 그것을하고 또한 그것을 시험했다 * /

1 단계 = viewDidLoad 메소드에서 다음과 같이 테이블에 대한 사용자 정의 셀 클래스 (표의 프로토 타입 셀의 경우) 또는 nib (사용자 정의 셀의 경우 사용자 정의 nib의 경우)를 등록하십시오.

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

또는

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

2 단계 = UITableView의 “dequeueReusableCellWithIdentifier : forIndexPath :”메소드를 다음과 같이 사용하십시오 (이를 위해서는 class 또는 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;
        }


답변

이 문제를 해결하는 여러 가지 프레임 워크가 있습니다. 몇가지 말하자면:

빠른:

목표 -C :


답변

스위프트 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
}


답변

@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()
            }


답변

가장 좋은 대답은이 작업을 수행하는 올바른 방법이 아닙니다. (실제로 indexPath를 모델과 바인딩했지만 항상 좋은 것은 아닙니다. 이미지를로드하는 동안 일부 행이 추가되었다고 상상해보십시오. 이제 주어진 indexPath의 셀이 화면에 있지만 이미지는 상황이 좀처럼 재현하기 어려울 수 있지만 가능합니다.

MVVM 접근 방식을 사용하고 컨트롤러에서 viewModel로 셀을 바인딩하고 viewModel에서 이미지를로드 한 다음 (reactiveCocoa 신호를 switchToLatest 메소드로 할당)이 신호를 구독하고 이미지를 셀에 할당하는 것이 좋습니다. 😉

MVVM을 남용하지 않아야합니다. 보기는 단순해야합니다! ViewModel은 재사용이 가능해야합니다! 컨트롤러에서 View (UITableViewCell)와 ViewModel을 바인딩하는 것이 매우 중요한 이유입니다.


답변

필자의 경우 이미지 캐싱 (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;
   }});
});