I'm new in swift programming and I have searched a lot about storing images with NSCache using Swift.
What I have done so far is that I'm getting ids and imageNames with JSON and I have my data in array and I was able to display image in cells with no problem. Now I want to cache images.
This is the code that I have written:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as TableViewCell
//cell.imageView
nameID = self.hoge[indexPath.row]["cars"]["carid"].string!
cell.nameLabel.text = nameID
if let imageFileURL = imageCache.objectForKey(self.hoge[indexPath.row]["cars"]["carid"].intValue) as? NSURL {
println("Get image from cache")
} else {
imageName = self.hoge[indexPath.row]["cars"]["pic_name"].string!
// If the image does not exist, we need to download it
var imgURL: NSURL = NSURL(string: "http://192.168.1.35/car/uploads/" + imageName )!
var image:UIImage = UIImage(named: "pen")!
// Download an NSData representation of the image at the URL
let request: NSURLRequest = NSURLRequest(URL: imgURL)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in
if error == nil {
image = UIImage(data: data)!
cell.viewCell.image = image
}
else {
println("Error: \(error.localizedDescription)")
}
})
}
return cell
}
SO, How can I store and retrieve images with cache?
I recommend you to check this library HanekeSwift (It provides a memory and LRU disk cache for UIImage, NSData, JSON, String or any other type that can be read or written as data), at least to understand how they are dealing with cache, and you can decide to use it or create your own solution.
Using a very easy/simple API:
// Setting a remote image
imageView.hnk_setImageFromURL(url)
// Setting an image manually. Requires you to provide a key.
imageView.hnk_setImage(image, key: key)
Using the cache
let cache = Shared.dataCache
cache.set(value: data, key: "image.png")
// Eventually...
cache.fetch(key: "image.png").onSuccess { data in
// Do something with data
}
Related
I have a TableView with ImageViews inside each cell. I want the images to get loaded once and remain like that but it seems that the images get loaded (downloaded, I'm getting them from an external API) as they get into visible area for user. It seems like a lazy load or something like that and I would like to disable it because if I scroll down then come back up most of the images get misplaced.
TableViewController.swift
cell?.mainChampImageView.image = businessLayer.getChampionThumbnailImage(championId: mainChampion.key)
BusinessLayer.swift
func getChampionThumbnailImage (championId: Int) -> UIImage {
return dataLayerRiot.getChampionThumbnailImage(championId: championId)
}
DataLayerRiot.swift
func getChampionThumbnailImage (championId: Int) -> UIImage {
var image: UIImage!
let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
let url = URL(string: urlString)
let session = URLSession.shared
let semaphore = DispatchSemaphore(value: 0)
session.dataTask(with: url!) {(data, response, error) in
if error != nil {
print("ERROR")
semaphore.signal()
}
else {
image = UIImage(data: data!)!
semaphore.signal()
}
}.resume()
semaphore.wait()
session.finishTasksAndInvalidate()
return image
}
Anyone know how to disable them loading as they get into visible area for the user and just have them "stored"?
EDIT
I am dequeuing the cell using the default way
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Match", for: indexPath) as? TableViewCell
...
}
Is there a better way of doing it?
EDIT 2
I also need to specify that I am unable to install libraries because this is a university project and I am able to work only on university's MACs (because I don't own one) therefore I am unable to install packages without administrator privileges.
You should save a task at memory like:
let task = = session.dataTask() {}
And after you can cancel it anywhere by:
task.cancel()
Alternatively, if the object session is a URLSession instance, you can cancel it by:
session.invalidateAndCancel()
Try SDWebImage for lazy loading the images in the UITableViewCell or UICollectionViewCell. Install it through cocoapods into your project.
It is an asynchronous memory + disk image caching with automatic cache expiration handling.
https://github.com/SDWebImage/SDWebImage
Code:
let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
let url = URL(string: urlString)
cell?.mainChampImageView.sd_setImage(with: url, placeholderImage: UIImage(named: "placeholder.png"))
It sounds like you could benefit from doing some image caching. There are multiple ways to go about doing so, but from your example, it doesn't look like you need to go through the trouble of adding an entire library to do so. You can do it in a simple manner using NSCache.
I created a class called ImageCache, and in this case it is a singleton, so that the cache is accessible throughout the entire application.
import UIKit
class ImageCache: NSObject {
static let sharedImageCache = ImageCache()
// Initialize cache, specifying that your key type is AnyObject
// and your value type is AnyObject. This is because NSCache requires
// class types, not value types so we can't use <URL, UIImage>
let imageCache = NSCache<AnyObject, AnyObject>()
// Here we store the image, with the url as the key
func add(image: UIImage, for url: URL) {
// we cast url as AnyObject because URL is not a class type, it's a value type
imageCache.setObject(image, forKey: url as AnyObject)
}
// This allows us to access the image from cache with the URL as the key
// (e.g. cache[URL])
func fetchImage(for url: URL) -> UIImage? {
var image: UIImage?
// Casting url for the same reason as before, but we also want the result
// as an image, so we cast that as well
image = imageCache.object(forKey: url as AnyObject) as? UIImage
return image
}
}
So now we have some relatively simple caching in place. Now for how to use it:
func getChampionThumbnailImage (championId: Int) -> UIImage {
var image: UIImage!
let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
let url = URL(string: urlString)
// Before, downloading the image, we check the cache to see if it exists and is stored.
// If so, we can grab that image from the cache and avoid downloading it again.
if let cachedImage = ImageCache.sharedImageCache.fetchImage(for: url) {
image = cachedImage
return image
}
let session = URLSession.shared
let semaphore = DispatchSemaphore(value: 0)
session.dataTask(with: url!) {(data, response, error) in
if error != nil {
print("ERROR")
semaphore.signal()
}
else {
image = UIImage(data: data!)!
// Once the image is successfully downloaded the first time, add it to
// the cache for later retrieval
ImageCache.sharedImageCache.add(image: image, for: url!)
semaphore.signal()
}
}.resume()
semaphore.wait()
session.finishTasksAndInvalidate()
return image
}
The reason the images are re-downloading is because a table view doesn't have unlimited cells. What happens is, as you scroll down, the cells that go off the screen are then recycled and re-used, so when you scroll back up, the images have to be grabbed again because they've been emptied out.
You can avoid downloading the images again by implementing caching.
Another way you can avoid having incorrect images is setting your image view to nil before you re-download the image. For example:
cell?.mainChampImageView = nil
cell?.mainChampImageView.image = businessLayer.getChampionThumbnailImage(championId: mainChampion.key)
All of the above, along with making sure that you are dequeuing cells properly should address your issue.
I have an image in tableview that is downloaded from a Json, everything works perfect but when scrolling before seeing the corresponding image it loads another for a few seconds (these images are those that are already visible in the table).
The structure of my data is:
struct Data: Decodable {
let name: String
let img: String
let phone: String
let linktaller: String
let web: String
}
The code of my cell where the image is loaded is:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as? AseguradorasTableViewCell else { return UITableViewCell() }
cell.titleLbl.text = company[indexPath.row].name
.
.
.
// load image
if let imageURL = URL(string: company[indexPath.row].img) {
DispatchQueue.global().async {
let data = try? Data(contentsOf: imageURL)
if let data = data {
let image = UIImage(data: data)
DispatchQueue.main.async {
cell.myImage.image = image
}
}
}
}
return cell
}
The function to load the data is:
func downloadJSON() {
let url = URL(string: "http://myserver.com/data.json")
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error == nil {
do {
self.company = try JSONDecoder().decode([Data].self, from: data!)
print(self.company)
DispatchQueue.main.async {
self.tableView.reloadData()
self.refreshControl.endRefreshing()
}
} catch let jsonError{
print("error + \(jsonError)")
}
}
}.resume()
}
See image for more detail:
Any suggestions are welcome to fix this problem.
In UITableView dequeueReusableCell- Each UITableViewCell will be reused several times with different data(image).
In your case, every cellForRowAt is called, the image will be load from server so it will have delay.
Solution: You must to cache image with url in local app when the image load finish.
(1)- Use SDWebImage - with cache support
(2)- You can save image in a array -> in cellForRowAt load from this array if existed and load from server if does not exist
(image from internet)
Add the following class for cache image support:
class ImageLoader {
var cache = NSCache<AnyObject, AnyObject>()
class var sharedInstance : ImageLoader {
struct Static {
static let instance : ImageLoader = ImageLoader()
}
return Static.instance
}
func imageForUrl(urlString: String, completionHandler:#escaping (_ image: UIImage?, _ url: String) -> ()) {
let data: NSData? = self.cache.object(forKey: urlString as AnyObject) as? NSData
if let imageData = data {
let image = UIImage(data: imageData as Data)
DispatchQueue.main.async {
completionHandler(image, urlString)
}
return
}
let downloadTask: URLSessionDataTask = URLSession.shared.dataTask(with: URL.init(string: urlString)!) { (data, response, error) in
if error == nil {
if data != nil {
let image = UIImage.init(data: data!)
self.cache.setObject(data! as AnyObject, forKey: urlString as AnyObject)
DispatchQueue.main.async {
completionHandler(image, urlString)
}
}
} else {
completionHandler(nil, urlString)
}
}
downloadTask.resume()
}
}
In the cell, load the image as follows:
// Load image
let fimage = company[indexPath.row].img
ImageLoader.sharedInstance.imageForUrl(urlString: fimage, completionHandler: { (image, url) in
if image != nil {
cell.myImage.image = image
}
})
With that, the download of the images should work correctly
Because of when ever the cell is showing, you download the image from internet by
let data = try? Data(contentsOf: imageURL)
You should
Check if image in imageURL has cached or not
If cached, load image from local
If not cache, download it from internet, then cache it.
Or just simple using SDWebImage or anything else, it will auto check the step 1 to 3 for you :D
For example by using SDWebImage
import SDWebImage
imageView.sd_setImage(with: URL(string: "your_image_url"))
This is a classic cell reuse problem. You should install a placeholder image, or nil, into the image view of each cell in your tableView(cellForRowAt:) method before you begin the download. That will clear out the previous image that was installed into the cell, and then the async download can run in the background and install the image once it's done loading.
To resolve similar issues, I changed my code to coordinate the downloading of images with the creation of tableView cells, storing the images in a local array.
I create a dictionary array to hold the downloaded images, using the url string as the key:
imagesArray = [String:UIImage]()
Then, at the point in the code where each image completes downloading, I add the image to the array and insert one new row into the tableView:
imagesArray.updateValue(UIImage(data: data!)!, forKey: imageURL as! String)
tableView.beginUpdates()
tableView.insertRows(at:[IndexPath(row: imagesArray.count-1, section: 0)], with: .automatic)
tableView.endUpdates()
tableView.reloadData()
I also maintain a separate array of information elements for each image, including the image url string as one element. This allows me to present the correct items in the tableView cell:
cell.itemNameLabel.text = itemRecords[indexPath.row].itemName
cell.itemImage.image = imagesArray[itemRecords[indexPath.row].imageURL]
While the images are downloading, I present a progress indicator spinner.
Once the images are all downloaded and are loaded into the imagesArray, there is NO delay in presenting as the user scrolls up and down to view the listed cells, and reused cells are loaded with the correct images.
Note: Please no libraries. This is important for me to learn. Also, there are a variety of answers on this but none that I found solves the issue nicely. Please don't mark as duplicate. Thanks in advance!
The problem I have is that if you scroll really fast in the table, you will see old images and flickering.
The solution from the questions I read is to cancel the URLSession
data request. But I do not know how to do that at the correct place
and time. There might be other solutions but not sure.
This is what I have so far:
Image cache class
class Cache {
static let shared = Cache()
private let cache = NSCache<NSString, UIImage>()
var task = URLSessionDataTask()
var session = URLSession.shared
func imageFor(url: URL, completionHandler: #escaping (image: Image? error: Error?) -> Void) {
if let imageInCache = self.cache.object(forKey: url.absoluteString as NSString) {
completionHandler(image: imageInCache, error: nil)
return
}
self.task = self.session.dataTask(with: url) { data, response, error in
if let error = error {
completionHandler(image: nil, error: Error)
return
}
let image = UIImage(data: data!)
self.cache.setObject(image, forKey: url.absoluteString as NSString)
completionHandler(image: image, error: nil)
}
self.task.resume()
}
}
Usage
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let myImage = images[indexPath.row]
if let imageURL = URL(string: myImage.urlString) {
photoImageView.setImage(from: imageURL)
}
return cell
}
Any thoughts?
Swift 3:
Flickering can be avoided by this way:
Use the following code in public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
cell.photoImageView.image = nil //or keep any placeholder here
cell.tag = indexPath.row
let task = URLSession.shared.dataTask(with: imageURL!) { data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async() {
if cell.tag == indexPath.row{
cell.photoImageView.image = UIImage(data: data)
}
}
}
task.resume()
By checking cell.tag == indexPath.row, we are assuring that the imageview whose image we are changing, is the same row for which the image is meant to be. Hope it helps!
A couple of issues:
One possible source of flickering is that while you're updating the image asynchronously, you really want to clear the image view first, so you don't see images for prior row of reused/dequeued table view cell. Make sure to set the image view's image to nil before initiating the asynchronous image retrieval. Or, perhaps combine that with "placeholder" logic that you'll see in lots of UIImageView sync image retrieval categories.
For example:
extension UIImageView {
func setImage(from url: URL, placeholder: UIImage? = nil) {
image = placeholder // use placeholder (or if `nil`, remove any old image, before initiating asynchronous retrieval
ImageCache.shared.image(for: url) { [weak self] result in
switch result {
case .success(let image):
self?.image = image
case .failure:
break
}
}
}
}
The other issue is that if you scroll very quickly, the reused image view may have an old image retrieval request still in progress. You really should, when you call your UIImageView category's async retrieval method, you should cancel and prior request associated with that cell.
The trick here is that if you're doing this in a UIImageView extension, you can't just create new stored property to keep track of the old request. So you'd often use "associated values" to keep track of prior requests.
I have a collection view which has 12 images I retrieve from a network call. I use NSCache to cache them. I want to know how I can delete a specific image from there? I have provided some code below to show how I cached the images. Thanks!
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("imageReuseCell", forIndexPath: indexPath) as! ImageCollectionViewCell
let image = hingeImagesArray[indexPath.row]
//Start animating activity indicator
cell.actitivityIndicator.startAnimating()
if let imageURL = image.imageUrl {
if let url = NSURL(string: imageURL) {
//Check for cached images and if found set them to cells - works if images go off screen
if let myImage = HomepageCollectionViewController.imageCache.objectForKey(image.imageUrl!) as? UIImage {
cell.collectionViewImage.image = myImage
}else {
// Request images asynchronously so the collection view does not slow down/lag
let task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
// Check if there is data returned
guard let data = data else {
print("There is no data")
return
}
if let hingeImage = UIImage(data: data){
//Cache images/set key for it
HomepageCollectionViewController.imageCache.setObject(hingeImage, forKey: image.imageUrl!)
// Dispatch to the main queue
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//Hide activity indicator and stop animating
cell.actitivityIndicator.hidden = true
cell.actitivityIndicator.stopAnimating()
//Set images to collection view
cell.collectionViewImage.image = hingeImage
})
}
})
task.resume()
}
}
}
return cell
}
NSCache is the smarter version of NSDictionary class which shares the same API for retrieving, adding or removing items.
Thus, you can delete an item from it using same method as if you do from a dictionary:
HomepageCollectionViewController.imageCache.removeObjectForKey(image.imageUrl!)
You can update your code to remove the image from cache that you are just about to show:
if let myImage = HomepageCollectionViewController.imageCache.removeObjectForKey(image.imageUrl!) as? UIImage {
// myImage was removed from cache.
cell.collectionViewImage.image = myImage
...
I have images in my collectionViewCell's that are fetched and parsed via NSURLRequest, how do I cache these images so they don't have to start a new request with every single appearance/disappearance of the view?
here is my code that fetches the images:
class funnyPicture: NSObject {
var pfPicture : PFObject
var coverImage : UIImage!
init(pfPicture: PFObject) {
self.pfPicture = pfPicture
}
func fetchCoverImage(completion: (image: UIImage?, error: NSError?) -> Void) {
let urlString = self.pfPicture["funnyPictures"] as! String
let url = NSURL(string: urlString)
let request = NSURLRequest(URL: url!)
let queue = dispatch_get_main_queue()
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) { (response: NSURLResponse?, data: NSData?, error: NSError?) in
if error == nil {
self.coverImage = UIImage(data: data!)
completion(image: self.coverImage, error: nil)
} else {
completion(image: nil, error: error)
}
}
}
}
and here is my collectionView code that parse the images to the collectionViewCell's:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! MyCollectionViewCell
// Configure the cell
let book = self.books[indexPath.row]
let coverImage = book.coverImage
if coverImage == nil {
book.fetchCoverImage({ (image, error) -> Void in
if self.collectionView != nil {
collectionView.reloadItemsAtIndexPaths([indexPath])
}
})
} else {
dispatch_async(dispatch_get_main_queue()){
let imageView = cell.imageView
imageView.image = book.coverImage
}
};
if book.coverImage == nil {
cell.imageView.userInteractionEnabled = false
cell.userInteractionEnabled = false
}else {
cell.imageView.userInteractionEnabled = true
cell.userInteractionEnabled = true
}
return cell
}
While I've received references to third party frameworks, I haven't received any answer on how to implement them with the code I have provided in the question, or even an answer using apples already implemented caching mechanism.. The reason I put the code in the question was for use in an answer.. Thank you.
Here is an example for your collection view cell:
import UIKit
let imageCache = NSCache<AnyObject, AnyObject>.sharedInstance
class myCell: UICollectionViewCell {
#IBOutlet public weak var myImageView: UIImageView?
private var imageUrlString: String?
private var downloadTask: URLSessionDownloadTask?
public var imageURL: URL? {
didSet {
self.downloadItemImageForSearchResult(imageURL: imageURL)
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
public func downloadItemImageForSearchResult(imageURL: URL?) {
if let urlOfImage = imageURL {
if let cachedImage = imageCache.object(forKey: urlOfImage.absoluteString as NSString){
self.myImageView!.image = cachedImage as? UIImage
} else {
let session = URLSession.shared
self.downloadTask = session.downloadTask(
with: urlOfImage as URL, completionHandler: { [weak self] url, response, error in
if error == nil, let url = url, let data = NSData(contentsOf: url), let image = UIImage(data: data as Data) {
DispatchQueue.main.async() {
let imageToCache = image
if let strongSelf = self, let imageView = strongSelf.myImageView {
imageView.image = imageToCache
imageCache.setObject(imageToCache, forKey: urlOfImage.absoluteString as NSString , cost: 1)
}
}
} else {
//print("ERROR \(error?.localizedDescription)")
}
})
self.downloadTask!.resume()
}
}
}
override public func prepareForReuse() {
self.downloadTask?.cancel()
myImageView?.image = UIImage(named: "ImagePlaceholder")
}
deinit {
self.downloadTask?.cancel()
myImageView?.image = nil
}
}
Don't forget to make an extension for NSCache
Like this:
import Foundation
extension NSCache {
class var sharedInstance: NSCache<NSString, AnyObject> {
let cache = NSCache<NSString, AnyObject>()
return cache
}
}
Use NSCache and NSOperationQueue to manage your image loading. There's a good post outlining the technique at https://stackoverflow.com/a/12721899/5271191 (It's Objective-C, but the technique is the same for Swift.)
I highly recommend you to use a clean in place replacement/extension for UIImageView, that will manage caching of the image all transparently to you and avoid unwanted complexity of maintaining operation queues, etc.
If in memory caching suffices your needs - check this out-
https://github.com/nicklockwood/AsyncImageView
If you want persistent caching, then this one will do-
https://github.com/rs/SDWebImage
HTH.
I have images in my collectionViewCell's that are fetched and parsed
via NSURLRequest, how do I cache these images so they don't have to
start a new request with every single appearance/disappearance of the
view?
The URL loading system already provides a cache. Take a look at the docs for NSURLCache. If the resources you need aren't already being sufficiently cached, you probably only need to adjust the disk space allocated to the URL cache for your app.
You should also take a look at the headers (cache-control, expires, etc.) that come back with your resources to make sure that they're not preventing caching. Here's a short tutorial on cache-related headers.
You should use a specialized framework for that. I would not recommend using SDWebImage, it is very outdated and is not stable.
Take a look at those two libraries that are up to date with iOS platform:
DFImageManager - advanced framework written in Objective-C but featuring nullability annotations (works great with Swift). Here's a list of things that make it better, than SDWebImage. Disclosure: it's written by me, opinion might be biased.
Kingfisher - lightweight library written in Swift. Similar to SDWebImage, but has much less features that SDWebImage and DFImageManager.
I have created a library using swift 2 to do the request for image and cache it. it's very simple just give it a try.
https://github.com/georgehadly/GHImageCaching
all you can do is something like this ,
viewImg.getCachedImage("geo", URI: NSURL(string: "https://s-media-cache-ak0.pinimg.com/236x/8e/5a/98/8e5a98795dc2c5322cac97343a6cad6d.jpg")!) { (done) -> Void in
if(done){
// your extra
}
}
in case you want to delete all cached images
UIImageView.deleteAllCaching()