I've been able to solve the issue of caching images to improve scroll performance in my app. However nil is found when it tries to add it to cache. Also how can I add a placeholder image for images that failed to load or aren't available ?
let imageCache = NSCache<NSString, UIImage>()
extension UIImageView {
func downloadImage(from imgURL: String) -> URLSessionDataTask? {
guard let url = URL(string: imgURL) else { return nil }
// set initial image to nil so it doesn't use the image from a reused cell
image = nil
// check if the image is already in the cache
if let imageToCache = imageCache.object(forKey: imgURL as NSString) {
self.image = imageToCache
return nil
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print(err)
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
imageCache.setObject(imageToCache!, forKey: imgURL as NSString)
self.image = imageToCache
}
}
task.resume()
return task
}
}
A couple of observations:
Just supply placeholder as parameter to function and use it instead of nil to initialize the image.
Do that after checking the cache (because there’s no point in using the placeholder if you found desired image in the cache).
Avoid use of ! forced unwrapping operator.
Check that UIImage(data:) found an image in the guard statement (and on the session queue, not the main thread).
Thus:
let imageCache = NSCache<NSString, UIImage>()
extension UIImageView {
func downloadImage(from imgURL: String, placeholder: UIImage? = nil) -> URLSessionDataTask? {
guard let url = URL(string: imgURL) else { return nil }
// check if the image is already in the cache
if let imageToCache = imageCache.object(forKey: imgURL as NSString) {
image = imageToCache
return nil
}
// set initial image to placeholder so it doesn't use the image from a reused cell
image = placeholder
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard
let data = data,
error == nil,
let imageToCache = UIImage(data: data)
else {
print(error ?? URLError(.badServerResponse))
return
}
imageCache.setObject(imageToCache, forKey: imgURL as NSString)
DispatchQueue.main.async {
self.image = imageToCache
}
}
task.resume()
return task
}
}
Related
I'm trying to download image and store that image in cache. From next attempt, pull that image from cache if exists otherwise download that image
I've gone thru similar posts but nothing helps me to fix.
Please advice
let imageCache = NSCache<NSString, UIImage>()
func getImage(from url: URL, completion: #escaping ((UIImage?, Error?)->(Void))) {
if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) {
print("Image from cache")
completion(cachedImage, nil)
}
NetworkOperations().downloadImage(from: url) { (data, error) in
if let error = error {
completion(nil, error)
} else if let imgData = data, let image = UIImage(data: imgData) {
self.imageCache.setObject(image, forKey: url.absoluteString as NSString)
print("Image from cache")
completion(image, nil)
}
}
}
When i call this method, it always picks by downloading image. I'm struck here
Create a shared cache at the application level and add/retrieve/remove wherever you want as below,
class ImageCache {
private init() {}
static let shared = NSCache<NSString, UIImage>()
}
Usage
func getImage(from url: URL, completion: #escaping ((UIImage?, Error?)->(Void))) {
if let cachedImage = ImageCache.shared.object(forKey: url.absoluteString as NSString) {
print("Image from cache")
completion(cachedImage, nil)
}
NetworkOperations().downloadImage(from: url) { (data, error) in
if let error = error {
completion(nil, error)
} else if let imgData = data, let image = UIImage(data: imgData) {
ImageCache.shared.setObject(image, forKey: url.absoluteString as NSString)
print("Image from cache")
completion(image, nil)
}
}
}
I hope, this will fix the issue and it should also give you an insight of what is being wrong with your implementation. Most probably your cache has the same scope as the object where it is declared and used so once the object is de-initialised, the cache will also be released.
In the above implemtation, cache is declared as a static so that it is never de-initialized but you can clear the cache anytime with the following,
ImageCache.shared.removeAllObjects()
What my requirement is "I have an application that downloads images from the amazon s3 bucket. And I need to cache those images, I used normal cache for doing it. But I need to implement the cache technique same as that of SDWebImage". How the caching method in SDWebImage works.
create a cache and set image to the url string and assign it from anywhere by checking the cache have the object or not
let imageCache = NSCache<AnyObject, AnyObject>()
imageCache.setObject(imageToCache!, forKey: urlString as AnyObject)
if let imageFromCache = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = imageFromCache
return
}
Pretty Handy UIImageview Extension for image caching.
import UIKit
let imageCache = NSCache<AnyObject, AnyObject()
extension UIImageView {
func cacheImage(urlString: String){
let url = URL(string: urlString)
image = nil
if let imageFromCache = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = imageFromCache
return
}
URLSession.shared.dataTask(with: url!) {
data, response, error in
if let response = data {
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
imageCache.setObject(imageToCache!, forKey: urlString as AnyObject)
self.image = imageToCache
}
}
}.resume()
}
}
I have an array filled with parsed json data including image url's. But when i try to see that images inside uiimageview, it doesn't the show. What should i do
I printed the url. This is my url inside array.
This is my array
var feedResult = [Result]()
It shows the name inside collectionview but i couldn't see the images. I used named like everybody does. But what is missing?
let info = feedResult[indexPath.row]
cell.appLabel.text = info.artistName
cell.customCollectionImage.image = UIImage(named: info.artWorkUrl)
You have to download the image Data using the url you got, only then you will use the downloaded data, like so:
imageView.image = UIImage(data: downloadedData)
Here is a quick subclass of UIImageView that does the downloading:
class URLImageView: UIImageView {
func download(url urlString: String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { (downloadedData, _, error) in
guard error == nil && downloadedData != nil else { return }
DispatchQueue.main.async{
self.image = UIImage(data: downloadedData!)
}
}
task.resume()
}
}
Update-1 Use the download function using UIImageView extension, without subclassing, like so:
extension UIImageView {
func download(url urlString: String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { (downloadedData, _, error) in
guard error == nil && downloadedData != nil else { return }
DispatchQueue.main.async{
self.image = UIImage(data: downloadedData!)
}
}
task.resume()
}
}
Usage:
cell.customCollectionImage.download(url: info.artWorkUrl)
By using UIImage(named: info.artWorkUrl) you are not accessing the image in your array but the images in your Assets.xcassets (assets that you add manually in your project).
You need to download the image from the artWorkUrl and then directly use the downloaded image like this:
cell.customCollectionImage.image = UIImage(data: yourImageData)
Where yourImageData is what you have downloaded from the server with the artWorkUrl.
I'm having problems cacheing for images from JSON correctly with this UIImageView extension. The images load correctly when I first open the app and scroll down the page. However when I scroll back up, they don't reload and are completely gone. Can anyone see anything wrong with the code?
let imageCache = NSCache<AnyObject, AnyObject>()
extension UIImageView {
func loadImageUsingUrlString(urlString: String) {
let url = NSURL(string: urlString)
if let imageFromCache = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = imageFromCache
return
}
URLSession.shared.dataTask(with: url! as URL) { (data, response, error) in
if error != nil {
print(error ?? "URLSession error")
return
}
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
imageCache.setObject(imageToCache!, forKey: urlString as AnyObject)
self.image = imageToCache
}
}.resume()
}
}
Here is the snippet from the cell.swift file
let imageCache = NSCache<AnyObject, AnyObject>()
func setupThumbnailImage() {
if let thumbnailImageUrl = television?.poster_url {
let urlPrefix = "https://www.what-song.com"
let urlSuffix = thumbnailImageUrl
let urlCombined = urlPrefix + urlSuffix
thumbnailImageView.loadImageUsingUrlString(urlString: urlCombined)
}
}
I suggest using kingFisher, it is very easy to use and it manages all starting from cache threads etc.
let imageResource = ImageResource(downloadURL:URL(string: imagePath )!,cacheKey: imagePath )
viewImage.kf.indicatorType = .activity
viewImage.kf.setImage(with: resource)
where imagePath is the url of your image and viewImage is your imageView
Most probably you would be calling it in wrong way.
Remember that in tableView you reuse the cells.
By the time response comes back for the URLSessionTask you would have already scrolled up/down. In that case self.image would be assigned to the currently visible cell.
Please add your cellForRow code in question.
I am new to iOS i want download image to display it is working code but here lot of code duplication
let url = URL(string: iteminfo.imageUrl!)
let urlRequest = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if error != nil {
print(error)
}
if let data = data {
print(data)
self.imageViewItemPic.image = UIImage(data: data)
}
}
task.resume()
let url2 = URL(string: iteminfo.cookerProfilePicUrl!)
let urlRequest2 = URLRequest(url: url2!)
let task2 = URLSession.shared.dataTask(with: urlRequest2) { (data, response, error) in
if error != nil {
print(error)
}
if let data = data {
print(data)
self.imageViewCookerProfilePic.image = UIImage(data: data)
}
}
task2.resume()
So I want to reuse my code but i unfortunately i can not reach my goal. there have no error and url is correct . every time goes else statement . i am missing something but what is that ?
if let image = downlaodImage(urlImage: iteminfo.imageUrl){
print("first \(image)")
imageViewItemPic.image = image
}else{
print("first wrong......")
}
if let image = downlaodImage(urlImage: iteminfo.cookerProfilePicUrl){
print("second \(image)")
imageViewCookerProfilePic.image = image
}
else{
print("second wrong......")
}
Here is my method :
func downlaodImage(urlImage : String?) -> UIImage?{
var image : UIImage?
let url = URL(string: urlImage!)
let urlRequest = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let data = data {
// print(data)
image = UIImage(data: data)
}
}
task.resume()
return image
}
note: I am not sure is it best way or not . if it is not best practice feel free to guide me .
There's no need of so much hassle. You have the URL of the image so you can simply download the image from the URL. For example:
func downloadImage(imageURL: String) {
DispatchQueue.global().async {
let data = NSData.init(contentsOf: NSURL.init(string: imageURL) as! URL)
DispatchQueue.main.async {
let image = UIImage.init(data: data as! Data)
imageView.image = image
}
}
}
Edit: To reuse this code I would suggest to use extension of UIImageView. Here's an example:
extension UIImageView {
func setImageFromURL(url: String) {
DispatchQueue.global().async {
let data = NSData.init(contentsOf: NSURL.init(string: url) as! URL)
DispatchQueue.main.async {
let image = UIImage.init(data: data as! Data)
self.image = image
}
}
}
}
Use this method whenever you want to set the image of an imageView from a url like this:
self.imageViewCookerProfilePic.setImageFromURL(url: "https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcQNpKmjx1w3DRDZ9IXN81-uhSUA6qL6obkOthoUkb9RZkXf5pJ8")
Dude. You should learn some staff about async and sync code.
Here is the thing. Code in you downloadImage works synchronically, so it pass you URLTask and go straight to return, there you return you image variable, that is nil.
One of the solutions in to use callback block like this:
func downloadImage(urlImage : String?, complete: ((UIImage?)->Void)? = nil){
let url = URL(string: urlImage!)
let urlRequest = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let data = data {
complete?(UIImage(data: data))
}
}
task.resume()
}
And then use it like this:
{ ...
downloadImage(urlImage: "", complete: { image in
if let image = image{
self.imageViewItemPic.image = image
}else{
print("no image")
}
})
...
}
You should read some tutorials about async code and web in swift. You could start with this site
downlaodImage() downloads an image asynchronously so
if let image = downlaodImage(...) { ... }
is always going to fail because program execution has continued before your response data has come back.
It would be easier just to set your images in the callback function closure of downlaodImage() as below by adding a UIImageView parameter to downlaodImage(). This way you can reduce the repetition of if else blocks by moving them to the downlaodImage function.
func downlaodImage(urlImage : String?, imageView: UIImageView) -> UIImage?{
var image : UIImage?
let url = URL(string: urlImage!)
let urlRequest = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let data = data {
// print(data)
if let image = UIImage(data: data) {
imageView.image = image
} else {
print("failed to load image")
}
}
}
task.resume()
return image
}
Simplified code without if/else blocks
downlaodImage(urlImage: "https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcQNpKmjx1w3DRDZ9IXN81-uhSUA6qL6obkOthoUkb9RZkXf5pJ8", imageView: imageViewItemPic)
downlaodImage(urlImage: "https://www.dominos.co.nz/ManagedAssets/OLO/eStore/all/Product/NZ/P015/P015_ProductImage_Small_en_Default_20140203_105245.png", imageView: imageViewCookerProfilePic)