내일배움캠프 iOS 2기

[트러블슈팅] 앱개발심화(개인) - 이미지 로딩 및 재사용 문제 해결

YEN_ 2024. 1. 31. 12:33

 

🩵 이미지 로딩 및 재사용 문제 해결

 

✔️ 문제 파악

- UITableViewCell 재사용 시 이미지뷰 초기화가 정상적으로 이루어지지 않아 이미지가 덮어씌워지는 문제.

- Main Thread에서 UI 업데이트에 대한 이슈.

< 문제 화면  /  정상 화면 >

 

 

 

 

✔️ 원인 분석

- 비동기 이미지 로딩을 하고 있는데, 이미지 로딩이 완료되기 전(imageView에 이미지 데이터가 설정되기 전)에 셀이 재사용되는 상황

(수정전)ImageLoader.swift

final class ImageLoader {
    static func loadImage(from url: String, into imageView: UIImageView, completion: (() -> Void)? = nil) {
        if let imageURL = URL(string: url) {
            let task = URLSession.shared.dataTask(with: imageURL) { (data, _, error) in
                if let error = error {
                    print("Error loading image: \(error.localizedDescription)")
                    return
                }
                else if let imageData = data, let image = UIImage(data: imageData) {
                    DispatchQueue.main.async {
                        imageView.backgroundColor = .clear
                        imageView.image = image
                        completion?()
                    }
                }
            }
            task.resume()
        }
    }
}

 

  1. ImageLoader에서 imageView를 전달받아 내부 image를 직접 업데이트 해주는 로직
  2. 비동기로 이미지 url을 로딩하고 그 로딩이 끝나면 imageView에 데이터를 세팅함
  3. 1회라면 imageView가 비어있기 때문에 괜찮지만, 2회째부터 문제가 발생하기 시작
  4. imageView가 세팅된 적이 있지만 초기화되지 않았기 때문에 비동기 로딩이 완료되기 전, 이미 불러와진 이미지 데이터를 세팅해서 cell을 재사용하는 것으로 생각됨
  5. 첫 번째 셀에 이미지를 로딩하고 있는 도중에 두 번째 셀이 이전 이미지뷰를 가져와서 이미지가 로딩되기 전에 두 번째 셀의 이미지뷰에 이미지가 설정될 수 있음. 이로 인해 셀의 이미지뷰에 예상치 못한 이미지가 표시되는 문제가 발생

 

 

 

- prepareForReuse에서 이미지뷰를 초기화해도 이미지 로딩이 완료되면 다시 설정되는 문제.

  1. 이 문제는 비동기 이미지 로딩과 셀의 재사용이 교차되면서 발생
  2. prepareForReuse에서 이미지뷰를 초기화하더라도, 이미지 로딩이 비동기적으로 이루어지고, 이미지 로딩이 완료되기 전에 다른 데이터로 인해 셀이 재사용되면 이미지뷰가 예상치 못한 이미지로 설정될 수 있음

 

 

 

 

✔️ 해결책

- ImageLoader를 UIImage 반환하는 로직으로 변경

(수정후)ImageLoader.swift

final class ImageLoader {
    static func loadImage(from url: String, completion: @escaping (Result<UIImage?, Error>) -> Void) {
        if let imageURL = URL(string: url) {
            let task = URLSession.shared.dataTask(with: imageURL) { (data, _, error) in
                if let error = error {
                    completion(.failure(error))
                }
                else if let imageData = data {
                    let image = UIImage(data: imageData)
                    
                    completion(.success(image))
                }
            }
            task.resume()
        }
    }
}

 

 

 

- UITableViewCell의 tag를 활용하여 비동기 이미지 로딩 완료 후 현재 셀의 상태를 확인.

- 이미지 로딩이 완료된 후에만 UI 업데이트가 이루어지도록 Main Thread에서 처리.

  • ImageLoader에 있던 ui 관련 코드를 클라이언트쪽으로 분리하여 단일책임원칙을 지킬 수 있게 수정함

PlayerTableViewCell.swift

func loadImage(from urlString: String) {
    let currentTag = self.tag
    print("currentTag: \(currentTag)")
    
    ImageLoader.loadImage(from: urlString) { result in
        // 이미지 로딩이 완료된 후에 설정됨
        DispatchQueue.main.async {
            if self.tag == currentTag {
                switch result {
                case .success(let image):
                    self.thumbnail.backgroundColor = .clear
                    self.thumbnail.image = image
                case .failure(let error):
                    print("Error decoding image: \(error)")
                    self.thumbnail.image = UIImage(systemName: "questionmark.video")
                }
                
            }
        }
    }
}

 

 

 

- 셀 재사용 시에는 이미지뷰의 이미지를 초기화하고, 완료된 이미지를 새로 설정.

PlayerTableViewCell.swift

override func prepareForReuse() {
    super.prepareForReuse()
    
    thumbnail.image = nil
}

 

 

 

 

 

 

 

 

https://developer.apple.com/documentation/uikit/uitableviewcell/1623223-prepareforreuse

 

prepareForReuse() | Apple Developer Documentation

Prepares a reusable cell for reuse by the table view’s delegate.

developer.apple.com

https://developer.apple.com/documentation/uikit/uitableview/1614878-dequeuereusablecell

 

dequeueReusableCell(withIdentifier:for:) | Apple Developer Documentation

Returns a reusable table-view cell object for the specified reuse identifier and adds it to the table.

developer.apple.com