Skip to content

Instantly share code, notes, and snippets.

@vinhnx
Last active July 3, 2019 10:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vinhnx/9b718d91d9dd39c8399833477831bc7d to your computer and use it in GitHub Desktop.
Save vinhnx/9b718d91d9dd39c8399833477831bc7d to your computer and use it in GitHub Desktop.
simple remote image loader
import Foundation
import UIKit
typealias DataMemoryCache = NSCache<NSString, NSData>
class ImageCache: DataMemoryCache {
static let shared: DataMemoryCache = {
let cache = DataMemoryCache()
cache.name = "ImageCache"
cache.countLimit = 20 // 20 items
cache.totalCostLimit = 10 * 1024 * 1024 // max megabytes in memory
return cache
}()
}
class RemoteImageLoader {
enum RemoteImageError: Error {
case invalidURL
case invalidData
case invalidContentType
case raw(_ error: Error)
}
static let shared = RemoteImageLoader()
private init() {} // This prevents others from using the default '()' initializer for this class.
private let imageCache = ImageCache.shared
private let processQueue = DispatchQueue(label: "RemoteImageLoader.queue", qos: .background)
private let outputQueue = DispatchQueue.main
// MARK: - Public
func loadImage(_ urlString: String,
completion: @escaping (Result<Data, RemoteImageError>) -> Void) {
if let existingImage = self.imageCache.object(forKey: urlString as NSString) {
return completion(.success(existingImage as Data))
}
guard let url = URL(string: urlString) else { return completion(.failure(.invalidURL)) }
self.processQueue.async {
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { return completion(.failure(.raw(error))) }
// expect image/ from response header
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.mimeType?.hasPrefix("image") == true
else { return completion(.failure(.invalidContentType)) }
self.outputQueue.async {
data.flatMap { data in
self.imageCache.setObject(
data as NSData,
forKey: urlString as NSString
)
completion(.success(data))
}
}
}.resume()
}
}
}
extension UIImageView {
func loadImage(_ url: URL) {
self.loadImage(url.absoluteString)
}
func loadImage(_ urlString: String, placeholder: String? = nil) {
placeholder.flatMap { image = UIImage(named: $0) }
RemoteImageLoader.shared.loadImage(urlString) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let data):
self.image = UIImage(data: data)
case .failure(let error):
print(error)
}
}
}
}
import XCTest
class RemoteImageLoaderTests: RemoteImageTests {
func testImageData() {
testImageLoader("https://picsum.photos/30/30", expectation: "expect to return image data", onSuccess: { data in
XCTAssertNotNil(data)
})
}
func testImageCache() {
let url = "https://picsum.photos/30/30"
testImageLoader(url, expectation: "expect to return image from cache", onSuccess: { data in
let cachedImage = ImageCache.shared.object(forKey: url as NSString)
XCTAssertNotNil(cachedImage)
XCTAssertTrue((cachedImage! as Data) == data,
"expect returned image was indeed from cache")
})
}
func testInvalidURL() {
testImageLoader("", expectation: "expect to return error") { error in
XCTAssertNotNil(error)
}
}
func testInvalidMIMEType() {
testImageLoader("https://xcodereleases.com/data.json", expectation: "expect to return invalid MIME error") { error in
if case .invalidContentType = error {
XCTFail()
}
}
}
func testImageViewLoader() {
testImageLoader("https://picsum.photos/30/30", expectation: "expect to UIImageView able to image data from remote URL", onSuccess: { data in
XCTAssertNotNil(data)
let image = UIImage(data: data)
XCTAssertNotNil(image)
let imageView = UIImageView(image: image!)
XCTAssertNotNil(imageView.image)
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment