Swift/Rick & Morty

[iOS] Rick&Morty - #12 Image Loader

devKen 2023. 3. 9. 16:47

#12

 

Image Loader를 만들고 안에서 캐시를 처리하여 효율적으로 처리하는 기능을 만든다.

 

BUG 제거

지난번에 한 번만 부르면 더 안 불러졌었다.

이유는 간단한데 fetchAdditionalCharacters에서 isLoadingMoreCharacter를 다시 false로 바꿔주는 작업을 우리가 이전에 주석처리하고 진행했기 때문이다.

 

final class RMCharacterListViewViewModel: NSObject {
    
    ...
    public func fetchAdditionalCharacters(url: URL) {
       ...
        RMService.shared.execute(request,
                                 expecting: RMGetAllCharacterResponse.self) { [weak self] result in
            ...
            switch result {
            case .success(let responseModel):
                ...
                DispatchQueue.main.async {
                    strongSelf.delegate?.didLoadMoreCharacters(
                        with: indexPathsAdd)
                     strongSelf.isLoadingMoreCharacters = false
                }
            ...
            }
        }
    }
    ...
}

 

 

아주 잘 출력되는 걸 볼 수 있다.

이제는 이미지를 로딩하는 Image Loader를 만들 것이며, 이미지를 캐싱하여 보관할 것이다.

 

ImageLoader

Manager 그룹에서 RMImageLoader라는 파일을 만들자. 이번에도 싱글톤으로 사용할 것이다.

import Foundation

final class RMImageLoader {
    static let shared = RMImageLoader()
    private init() {}
    func downloadImage(_ url: URL) {}
}

 

downloadImage 안에 여러 가지를 채워 넣어야 하는데 우리가 예전에 만들어 놨던 RMCharacterCollectionViewCellViewModel의 fetchImage에서 가져오자.

completion으로 받아야 하는 걸 잊으면 안 된다.

final class RMImageLoader {
    static let shared = RMImageLoader()
    private init() {}
    
    public func downloadImage(_ url: URL, completion: @escaping (Result <Data, Error>) -> Void) {
        let request = URLRequest(url: url)
        let task  = URLSession.shared.dataTask(with: request) { data, _, error in
            guard let data = data, error == nil else {
                completion(.failure(error ?? URLError(.badServerResponse)))
                return
            }
            completion(.success(data))
        }
        task.resume()
    }
}

원래 가져왔던 곳에서는 싱글톤을 이용하여 메서드로 호출을 하자. 간단한 작업이지만 코드의 재사용성이 눈에 띄게 증가했다.

public func fetchImage(completion: @escaping (Result <Data, Error>) -> Void) {
        guard let url = characterImageUrl else {
            completion(.failure(URLError(.badURL)))
            return
        }
        RMImageLoader.shared.downloadImage(url, completion: completion)
    }

 

Cache

컬렉션뷰에서 만약 스크롤해서 아래로 내려가게 되면 화면에서 사라진 셀은 디큐 된다. 그래서 다시 위로 올라가면 사실 다시 다운로드하는 것이다.

  • dequeue - 여러 가지 상황에서 쓸 수 있으나 이 경우 기존에 생성된 셀을 새로운 셀로 업데이트하고 재사용큐에 반환하는걸 cell dequeue라고 한다. 

Cache를 사용하여 데이터를 저장해 놓고 사용해 보자. 우린 NSCache를 사용할 것이다. NSCache는 자동적으로 메모리 관리 시 필요한 만큼 저장해 놓기 때문에 성능에 큰 이슈가 없다는 좋은 점이 있다.

NSCache는 정의된 곳을 들어가면 KeyType과 ObjectType으로 선언해야 하는 것을 알 수 있다. 이를 참고하여 캐시를 만들고 만약 이미지가 캐시 안에 존재하면 API 통신을 하지 않고 꺼내다 쓰는 것으로 바꿔보자

imageDataCache라는 stored Propety를 NSCache로 선언한다. NSCache는 클래스만을 받을 수 있기 때문에 NSString, NSData로 선언해 주자.

그리고 task에서 이미지를 받아 올 때 imageDataCache에 저장한다. 잘 보면 setObject로 하고 있다. 리스트가 아니기 때문이다. 그리고 메서드 초반에 key(=이미지 주소)에 해당하는 데이터가 있다면 API를 사용하지 않고 바로 넣어주자

final class RMImageLoader {
    
    private var imageDataCache = NSCache<NSString, NSData>()
    ...
    public func downloadImage(_ url: URL, completion: @escaping (Result <Data, Error>) -> Void) {
        let key = url.absoluteString as NSString
        if let data = imageDataCache.object(forKey: key) {
            completion(.success(data as Data))
            return
        }
        ...
        let task  = URLSession.shared.dataTask(with: request) { [weak self] data, _, error in
            ...
            let key = url.absoluteString as NSString
            let value = data as NSData
            self?.imageDataCache.setObject(value, forKey: key)
            completion(.success(data))
        }
        task.resume()
    }
}

 

그런데 어딘가에 버그가 있다. 스크롤을 계속 내리다 보면 터진다. 잠깐 언뜻 본 바로는 cell의 숫자가 안 맞아서 생기는 경우인 듯하다.