본문 바로가기

iOS 개발/Unsplash 클론 코딩

Unsplash - 5. 다운로드 가능한 이미지뷰와 이미지 캐싱

반응형

안녕하세요. Skillist입니다.

 

오늘은 지난글에 나왔던, DownloadableImageView를 알아보죠.

이름 그대로, url을 통해서 이미지를 다운받아 보여줄 수 있는 ImageView에요.

ImageView를 상속했어요.

 

이미지를 쉽게 다운받을 수 있는 방법은 강력한 라이브러리인 "kingfisher"를 사용하면 돼요.

코코아팟, 스위프트 패키지 매니터, 카르타고 등 추가할 수 있구요.

사용하기도 쉽고 강력해요.

https://github.com/onevcat/Kingfisher

 

GitHub - onevcat/Kingfisher: A lightweight, pure-Swift library for downloading and caching images from the web.

A lightweight, pure-Swift library for downloading and caching images from the web. - GitHub - onevcat/Kingfisher: A lightweight, pure-Swift library for downloading and caching images from the web.

github.com

 

그치만 제가 KingFisher를 사용하지 않은 이유는 왜일까요?

KingFisher를 사용하면 편하지만, 이미지 다운로드를 직접 구현하고 싶었어요. 

제가 알기론, 코딩 테스트를 볼땐 오픈소스 사용 금지와 같은 제약 사항들이 있습니다.

이런 제약 사항들은 개발자의 기본기를 알아보기 위함이 아닐까요?

따라서, 기본기를 좀 쌓고 싶어서 직접 구현했어요.

Unsplash 앱을 보면 알겠지만, 모든 이미지는 다운로드하여 보여줘야 하기 때문에,

ImageView를 상속한 DownloadableImageView를 만들었어요.

 

그치만 SnapKit 라이브러리를 사용한 이유는????

SnapKit을 학습하기 위함이었죠~.

 

 

다함께 코드를 살펴보시죠.

 

먼저, DownloadableImageView에 UI레이아웃을 추가했어요.

다운 받는 중이라면, indicator를 통해서, 로딩을 표시해주고,

다운로드 실패 시, 사용자에게 보여줄 text에 대한 textView 입니다.

 

SnapKit을 통해, 레이아웃 제약사항을 추가했어요. 간단하죠?

 

이번엔 download 관련 로직입니다.

14라인 : 이미지를 다운 받는 task 저장 프로퍼티

 

17라인 : 다운로드 취소에 대한 저장 프로퍼티

 

26라인 : 다운로드 실패하였을때에 대한 저장 프로퍼티

 

다운로드를 취소하는 경우는 언제일까요?

생각해 보고 아래로 스크롤 내려보세요.

 

-------------------------------------------------------------------------------------------

 

아마 다들 아실텐데요. UICollectionView는 셀을 재사용할 수 있습니다.

만약 100개의 셀이 필요한다고 가정해볼게요.

100개의 셀을 만드는게 아니라, 일부의 셀만 생성하고, 셀을 재사용하여 셀 100개가 생성된것 처럼 보여주죠.

셀이 재사용되기 때문에, 이전 사진 정보에 대한 이미지 다운로드가 계속 진행되고, 업데이트될 수 있어요.

따라서, 셀이 재사용 된다면, 기존 이미지 다운로드를 취소하고, 새로운 이미지를 다운받아야겠죠?

이를 위해서 "isCancel"이 사용됩니다. isCancel이 true로 설정되면, 20라인을 통해 진행중인 task를 중지합니다.

 

다운로드가 실패했을때, 사용자에게 이미지 다운 실패를 알려줘야겠죠? 그렇지 않다면, 사용자가 계속 기다릴 수 있잖아요.

따라서, 다운로드 중일땐, activityIndicatorView를 통해 사용자에게 알려주고, 

다운로드 실패하면, label을 통해서 사용자에게 다운 실패를 알려줄거에요.

 

이제 이미지 다운 로직을 살펴볼 차례인데요, 먼저 캐시를 한번 볼게요.

 

ImageCache라는 클래스를 만들고 static으로 NSCache을 할당했습니다.

url 주소를 키로, UIImage를 캐싱하게 되죠.

 

만약 다운로드한 이미지가 ImageCache에 존재한다면 다시 다운받을 필요 없잖아요?? 바로 가져다 쓰면 되죠.

캐시까지 이해 됐죠??

 

그럼 이미지를 다운 해봅시다.

84, 85, 86라인 : 이미지를 nil로 변경하고, 이전에 취소하거나 실패한 다운로드 값을 false로 변경합니다.

 

89라인 : 이미지 캐시를 확인하여, 존재한다면, 다운로드 하지 않고 이미지를 적용합니다.

 

94라인 : URL의 유효성 확인합니다. 

 

100라인 : 다운로드 시작 시 사용자에게 알림

 

102라인 : 이미지를 dataTask를 통해 가져옵니다.

 

107라인 : dataTask를 취소한 경우 종료합니다.

 

113라인 : 가져온 결과값을 확인하여, 이미지를 가져오지 못했다면 fail처리합니다.

 

121라인 : 이미지 캐시에 추가합니다.

 

124라인 : 만약 이미지 다운 취소를 한 경우, 다운로드한 이미지를 이미지뷰에 적용하지 않습니다.

 

129라인 : 이미지뷰에 이미지를 적용하고, loadingView에 대한 애니메이션을 멈추고 숨깁니다. 28라인을 보면, 애니메이션이 멈추면, loadingView를 숨기게끔 설정해놨죠.

 

134라인 : 다운로드를 시작합니다.

 

이렇게 ImageView에 다운로드한 이미지를 캐시하고 적용하는 DoanloadableImageView를 구현해봤어요.

 

cancel 로직이 reuseCell에서 은근 중요하더라구요.

중요성을 알기 위해서, isCancel 관련 로직을 싹다 주석처리하고 이미지 다운 받아보세요.

두번째셀에 보여줘야할 이미지가 여섯번째에 보여지거나, 이미지들이 중복해서 보여지거나, 이미지가 갑자기 변경되는 등 다이나믹 해집니다.

꼭 경험해보세요.

 

 

잘못되거나 부족한 내용 등, 피드백 감사합니다!

 

Skillist의 Unsplash 프로젝트

https://github.com/DeveloperSkillist/UnsplashCloneCode

 

GitHub - DeveloperSkillist/UnsplashCloneCode: UnsplashCloneCode

UnsplashCloneCode. Contribute to DeveloperSkillist/UnsplashCloneCode development by creating an account on GitHub.

github.com

 

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓  전체 코드  ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

import UIKit
import SnapKit

//이미지를 다운로드할 수 있는 이미지뷰
class DownloadableImageView: UIImageView {
    
    var dataTask: URLSessionTask?
    
    //이미지뷰의 다운로드 취소 여부 확인
    var isCancel: Bool = false {
        willSet {
            if newValue {
                dataTask?.cancel()
            }
        }
    }
    
    //이미지 다운로드 실패 시 이미지뷰 처리
    private var isFail: Bool = false {
        willSet {
            DispatchQueue.main.async { [weak self] in
                self?.textView.isHidden = !newValue
                self?.loadingView.stopAnimating()
            }
        }
    }
    
    //이미지 뷰에서 다운로드 중을 보여줄 인디케이터
    private lazy var loadingView: UIActivityIndicatorView = {
        let loadingView = UIActivityIndicatorView()
        loadingView.hidesWhenStopped = true
        loadingView.stopAnimating()
        return loadingView
    }()
    
    //이미지 뷰에서 다운로드 실패를 알려줄 인디케이터
    private lazy var textView: UILabel = {
        let textView = UILabel()
        textView.text = "image_load_fail".localized
        textView.font = .systemFont(ofSize: 14, weight: .bold)
        textView.textColor = .white
        textView.textAlignment = .center
        textView.isHidden = true
        textView.numberOfLines = 0
        return textView
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        setupLayout()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    private func setupLayout() {
        [
            loadingView,
            textView
        ].forEach {
            self.addSubview($0)
        }
        
        loadingView.snp.makeConstraints {
            $0.centerX.centerY.equalToSuperview()
        }
        
        textView.snp.makeConstraints {
            $0.edges.equalToSuperview().inset(10)
        }
    }
    
    //이미지 Url 입력하여 다운롣,
    func downloadImage(url: String) {
        self.image = nil
        isFail = false
        isCancel = false
        
        //이미지 캐시하여, 이미지가 존재하면, 이미지 적용
        if let image = ImageCache.shared.object(forKey: url as NSString) {
            self.image = image
            return
        }
        
        guard let url = URL(string: url) else {
            self.isFail = true
            return
        }
        
        //이미지 다운로드 시작을 사용자에게 알림
        loadingView.startAnimating()
        
        dataTask = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard let self = self else {
                return
            }
            
            if let error = error as NSError?,
               error.code == -999 {
                print("error : \(error.localizedDescription)")
                return
            }
            
            guard let data = data,
                  let image = UIImage(data: data) else {
                      self.isFail = true
                      print("imageDownload error : \(error.unsafelyUnwrapped.localizedDescription)")
                      return
                  }
            
            //이미지 캐시
            ImageCache.shared.setObject(image, forKey: url.absoluteString as NSString)
            
            //이미지 다운을 취소한경우 중
            if self.isCancel {
                return
            }
            
            //다운받은 이미지 적용
            DispatchQueue.main.async {
                self.image = image
                self.isFail = false
            }
        }
        dataTask?.resume()
    }
}
import UIKit

//이미지 캐시
class ImageCache {
    static let shared = NSCache<NSString, UIImage>()
}
반응형