Caching images in collection view - ios

I am stuck on how to fix something. I currently have an iPhone app that is literally Instagram but just horizontally and paging, where users see friends photos.
Now I have created a function that grabs images from firebase and puts them in an array. Now this works great along with performing a shared URLSession. I noticed my app was running high on memory usage so I added a URLcache, and set the limit on how large it can get, actually is sort of high now that I think about it. But I am still getting high memory (171mb)-, and that's only loading 4 images usage which makes it seem like I am not caching the data right.
I am still learning how to work with URLSessions and also caching so this also might contribute to a problem if I set it up wrong. Online people were saying use SDWebImage, but really, users won't scroll down or be able to scroll down fast because first paging's enabled and also it's horizontal. Here is some of my code, please tell me what you think I should do.
urlcache in
viewDidLoad() { //probably too high..
let memoryCapacity = 500 * 1024 * 1024
let diskCapacity = 500 * 1024 * 1024
let urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: "myDiskPath")
URLCache.shared = urlCache
}
// cellforrow
if posts.count != nil {
let pozt = posts[indexPath.row].pathToImage
let url = URL(string: pozt!)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if error != nil {
print(error?.localizedDescription as Any)
return
}
DispatchQueue.main.async {
cell.myImage.image = UIImage(data: data!)
self.loader.stopAnimating()
}
}).resume()
}
cell.myImage.image = UIImage(named: imaqes[0])
return cell
}

I think almost all programmers on swift using libraries to cache images.
I use KingFisher instead of SDWebImage. Its lightweight and simple to use.
To install:
Podfile:
pod 'Kingfisher'
In terminal:
pod install
In swift file:
import Kingfisher
In your case use it next way:
// cellforrow
if posts.count != nil {
let pozt = posts[indexPath.row].pathToImage
let url = URL(string: pozt!)
DispatchQueue.main.async {
cell.myImage.kf.setImage(with: url!) //Using kf for caching images
}
}
return cell
Maybe you should use .kf.setImage with comletion handler to remove loader. You should get the idea.
Hope it helps

Related

IOS app crashes on a line of code if there's no internet connection, how can I prevent this

The code this and it crashes on "try!", but I don't know how to catch the error and it has it be explicit otherwise it won't work.
func downloadPicture2(finished: () -> Void) {
let imageUrlString = self.payments[indexPath.row].picture
let imageUrl = URL(string: imageUrlString!)!
let imageData = try! Data(contentsOf: imageUrl)
cell.profilePicture.image = UIImage(data: imageData)
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
}
The short answer is don't use try! - Use do/try/catch and recover from the problem in the catch clause.
For example -
func downloadPicture2(finished: () -> Void) {
cell.profilePicture.image = nil
if let imageUrlString = self.payments[indexPath.row].picture,
let imageUrl = URL(string: imageUrlString) {
do {
let imageData = try Data(contentsOf: imageUrl)
cell.profilePicture.image = UIImage(data: imageData)
}
catch {
print("Error fetching image - \(error)")
}
}
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
}
Now you have code that won't crash if the url is invalid or there is no network, but there are still some serious issues with this code.
Data(contentsOf:) blocks the current thread while it fetches the data. Since you are executing on the main thread this will freeze the user interface and give a poor user experience.
Apple specifically warns not to do this
Important
Don't use this synchronous initializer to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated.
Rather, you should use an asynchronous network operations, such as a dataTask.
This code operates on cell - an external property. Once you move to asynchronous code you will probably be fetching images for multiple cells simultaneously. You should pass the relevant cell to this function to avoid clashes.
The use of the network isn't particularly efficient either; assuming this is part of a table or collection view, cells are reused as the view scrolls. You will repeatedly fetch the same image as this happens. Some sort of local caching would be more efficient.
If it is possible to use external frameworks in your project (i.e. your employer doesn't specifically disallow it) then I strongly suggest you look at a framework like SDWebImage or KingFisher. They will make this task much easier and much more efficient.

Large image URL response not cached by URLSession: Why?

I use a URLSession data task to download several JPG images from a backend. As the images are rather large in size (~500 KB) I want to cache the respective responses until they have expired (i.e. they have exceeded their max-age).
This is the code I use for downloading the images:
let request = URLRequest(url: url,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
let task = URLSession.shared.dataTask(with: request) { (data, _, error) in
// Error:
guard let imageData = data, error == nil, let image = UIImage(data: imageData) else {
DispatchQueue.main.async {
completion(nil)
}
return
}
// Success:
DispatchQueue.main.async {
completion(image)
}
}
task.resume()
Curiously, this works great with caching for all images except for one. For some reason, this particular image is always downloaded again – its response is not cached.
The only difference between the responses that I can spot is that the image whose corresponding response is not cached has the biggest file size. While all other images are < 500 kB, this particular image is slightly > 500 kB.
I've played around with the shared cache size and set it to a ridiculously high value, with no effect:
URLCache.shared = URLCache(memoryCapacity: 1000 * 1024 * 1024,
diskCapacity: 1000 * 1024 * 1024,
diskPath: nil)
I've checked that the Cache-Control header field is correctly set in the response:
Cache-Control: public, max-age=86400
and the Age header field is always below max-age, for example:
Age: 3526
What could be the reason for a single response not to be cached?
How can I fix this?
This is not an answer to the question why the shared URLSession does not cache the image and I'm still grateful for any hints or answers to that question.
However, after experimenting some time with my code I figured out that (for whatever reason) the response is always being cached when I use a custom URL session with a default configuration rather than the default shared URL session:
let urlSession = URLSession(configuration: .default)
So if I use:
let task = urlSession.dataTask(with: request) { ... }
instead of
let task = URLSession.shared.dataTask(with: request) { ... }
the caching works as expected – whatever black magic is responsible for that. 🤔
I found a little hint in the docs for URLSession.shared though:
When working with a shared session, you should generally avoid
customizing the cache, ...
In other words, if you’re doing anything with caches, cookies,
authentication, or custom networking protocols, you should probably be
using a default session instead of the shared session.

SDWebImage's sd_setImageWithURL updates the wrong cell with Image on scrolling!! Is that a expected behaviour?

OverView
I am downloading the images from web server using SDWebImage in my collectionView cell.
if floor.hasTitleImage != nil {
self.floorImageView.sd_setImageWithURL(NSURL(string:(floor.hasTitleImage?.imageURL)!), placeholderImage: UIImage(named: "Placeholder"), completed: { (imageDownloaded, error, cacheType, url) in
if imageDownloaded.size.width > 300 {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let resizedImage = imageDownloaded.resizePreservingAspectRatio(toSize: CGSize(width: 300, height: 300))
SDImageCache.sharedImageCache().removeImageForKey(NSURL(string:(floor.hasTitleImage?.imageURL)!)?.absoluteString, fromDisk: true, withCompletion: {
SDImageCache.sharedImageCache().storeImage(resizedImage, forKey: NSURL(string:(floor.hasTitleImage?.imageURL)!)?.absoluteString, toDisk: true)
})
})
}
})
}
else{
self.floorImageView.image = UIImage(named: "Placeholder")
}
Now what this code is doing is downloading the image using the url stored in core data element called floor. Now the images downloaded are way bigger then my ImageView so am resizing it to somewhere around 300px.
I know that SDWebImage does intense caching by default and uses url's absolute string as key for its cache. So Once resized! I replace the original image with my resized image.
Issue
Everything worked fine as expected! till I tested it with low internet speed which caused the image download to be delayed.
If the image downloading takes time, till then if I scroll the tableView and the cell gets reused, once the image downloaded SDWebImage loads the imageView of the cell but ends up resulting the wrong cell with wrong image.
Though this happens only once as for next time onwards image will be loaded from cache everything works fine. But Is this the expected behaviour or am I doing something wrong???
Solutions I could think of :
As sd_setImageWithURL by default sets the value of imageView with downloaded image rather than downloading the image using sd_setImageWithURL, if I can download it using,
if floor.hasTitleImage != nil {
if let downloadedImage = SDImageCache.sharedImageCache().imageFromMemoryCacheForKey(NSURL(string:(floor.hasTitleImage?.imageURL)!)?.absoluteString){
self.floorImageView.image = downloadedImage
}
else if let diskImage = SDImageCache.sharedImageCache().imageFromDiskCacheForKey(NSURL(string:(floor.hasTitleImage?.imageURL)!)?.absoluteString){
self.floorImageView.image = diskImage
}
else{
let downloadManager = SDWebImageManager.sharedManager();
downloadManager.downloadImageWithURL(NSURL(string:(floor.hasTitleImage?.imageURL)!), options: [], progress: nil, completed: { (downloadedImage, error, cacheType, finished, url) in
if downloadedImage.size.width > 300 {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let resizedImage = downloadedImage.resizePreservingAspectRatio(toSize: CGSize(width: 300, height: 300))
SDImageCache.sharedImageCache().removeImageForKey(NSURL(string:(floor.hasTitleImage?.imageURL)!)?.absoluteString, fromDisk: true, withCompletion: {
SDImageCache.sharedImageCache().storeImage(resizedImage, forKey: NSURL(string:(floor.hasTitleImage?.imageURL)!)?.absoluteString, toDisk: true)
})
if floor.hasTitleImage?.imageURL != nil && floor.hasTitleImage?.imageURL! == url{
dispatch_async(dispatch_get_main_queue(), {
self.floorImageView.image = SDImageCache.sharedImageCache().imageFromMemoryCacheForKey(NSURL(string:(floor.hasTitleImage?.imageURL)!)?.absoluteString)
self.reloadInputViews()
})
}
})
}
})
}
}
else{
self.floorImageView.image = UIImage(named: "Placeholder")
}
Though it works it will load the image only when I scroll the tableView :(
Questions :
Is what sd_setImageWithURL doing is expected behaviour ?? I mean every single one of the app using SDWebImage with tableView or CollectionView must have faced this correct ?? So if no body faced it I must be doing something wrong :( Am I doing anything wrong ?
Is what am doing using SDWebImageManager.sharedManager is correct ??? Is that long code required? Checking memory cache then disk cache and finally making call to download image isn't it too much :o
Why is that my second approach not loading the image once downloaded and expects the collection view to scroll and reload the cell to display the image ??
EDIT
I solved the 3rd question :) Sorry my mistake. I was comparing if floor.hasTitleImage?.imageURL != nil && floor.hasTitleImage?.imageURL! == url{ which is wrong because url above is NSURL where as floor.hasTitleImage?.imageURL! is NSString.
So Updated solution :
if floor.hasTitleImage?.imageURL != nil && floor.hasTitleImage!.imageURL! == url.absoluteString{
dispatch_async(dispatch_get_main_queue(), {
self.floorImageView.image = SDImageCache.sharedImageCache().imageFromDiskCacheForKey(NSURL(string:(floor.hasTitleImage?.imageURL)!)?.absoluteString)
self.setNeedsLayout()
})
}
Though my second approach downloads the image now and updates the cell properly but now image downloading is very slow compared to sd_setImageWithURL.
Is there any way I can use the method1 sd_setImageWithURL and get the image updated properly?
Please help me!! what am doing wrong !! Thanks in advance :)
Is what sd_setImageWithURL doing is expected behaviour ?? I mean every single one of the app using SDWebImage with tableView or CollectionView must have faced this correct ?? So if no body faced it I must be doing something wrong :( Am I doing anything wrong ?
No, it's not expected behaviour. I'm using SDWEbImage as well both tableView and collectionView and both are work fine.
Is what am doing using SDWebImageManager.sharedManager is correct ??? Is that long code required? Checking memory cache then disk cache and finally making call to download image isn't it too much :o
You don't have to cache image youself, SDWebImage take care caching process for you (in memory and disk).
If you need to resize image before cache and display, I think better do it by implement SDWebImageManagerDelegate which allows you to transform the image after it has been downloaded but before cache and display.
UPDATE:
If you use sd_setImageWithURL, there is another solution to prevent wrong assign image is by override prepareForReuse() method in UICollectionViewCell subclass and call sd_cancelCurrentImageLoad() on imageView and set image to nil.
hope this help.

Using dispatch_async to load images in background

I am trying to update a progress bar as my images load. I've read several answers here and tried formatting my code many different ways. I'm trying to read in the images and update the progress bar. Then, will all the images are loaded call the code to process them. The best result I've got was some code that works most of the time. However, if I'm dealing with a situation where it is pulling in a lot of images, I get weird errors. I think it is going ahead and running the continue code before all the images are fully loaded. When I remove the dispatch_async, the code works fine but the progress bar does not update.
func imageLocXML(il:String) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
let url:NSURL? = NSURL(string: il)
let data:NSData? = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
pieceImages.append(image!)
self.numImagesLoaded += 1
self.updateProgressBar()
if self.numImagesLoaded == self.numImagesToLoad {
self.continueLoad()
}
}
}
There a number of issues:
This code isn't thread safe, because you have race condition on numImagesLoaded. This could, theoretically, result in continueLoad to be called more than once. You can achieve thread safety by synchronizing numImagesLoaded by dispatching updates to this (and other model objects) back to the main queue.
Like DashAndRest said, you have to dispatch the UI update to the main queue, as well.
When you made this asynchronous, you introduced a network timeout risk when you initiate a lot of requests. You can solve this by refactoring the code to use operation queues instead of dispatch queues and specify maxConcurrentOperationCount.
The images are being added to an array:
Because these tasks run asynchronously, they're not guaranteed to complete in any particular order, and thus the array won't be in order. You should save the images in a dictionary, in which case the order no longer matters.
Just like numImagesLoaded, the pieceImages isn't thread safe.
You are using a lot of forced unwrapping, so if any requests failed, this would crash.
But to address this, we have to step back and look at the routine calling this method. Let's imagine that you have something like:
var pieceImages = [UIImage()]
func loadAllImages() {
for imageUrl in imageURLs {
imageLocXML(imageUrl)
}
}
func imageLocXML(il:String) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
let url:NSURL? = NSURL(string: il)
let data:NSData? = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
self.pieceImages.append(image!)
self.numImagesLoaded += 1
self.updateProgressBar()
if self.numImagesLoaded == self.numImagesToLoad {
self.continueLoad()
}
}
}
I'm suggesting that you replace that with something like:
var pieceImages = [String: UIImage]()
func loadAllImages() {
let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 4
let completionOperation = NSBlockOperation {
self.continueLoad()
}
for imageURL in imageURLs {
let operation = NSBlockOperation() {
if let url = NSURL(string: imageURL), let data = NSData(contentsOfURL: url), let image = UIImage(data: data) {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.numImagesLoaded += 1
self.pieceImages[imageURL] = image
self.updateProgressBar()
}
}
}
queue.addOperation(operation)
completionOperation.addDependency(operation)
}
NSOperationQueue.mainQueue().addOperation(completionOperation)
}
Having said that, I think there are deeper issues here:
Should you be loading images in advance like this at all? We would generally advise lazy loading of images, only loading them when needed.
If you're going to load images into a structure like this, you should gracefully handle memory pressure, purging it upon low memory warning. You then need to gracefully handle what to do when you go to retrieve an image and it's been purged due to memory pressure (leading you right back to a just-in-time lazy loading pattern).
We'd generally advise against synchronous network requests (the NSData(contentsOfURL:_)). We'd generally use NSURLSession which is cancellable, offers richer error handling, etc. Admittedly, that complicates the above code even further (probably leading me down the road of asynchronous NSOperation subclass), but you should at least be aware of the limitations of contentsOfURL.
Try DISPATCH_QUEUE_PRIORITY_DEFAULT this for background queue as:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
let url:NSURL? = NSURL(string: il)
let data:NSData? = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
self.pieceImages.append(image!)
self.numImagesLoaded += 1
dispatch_async(dispatch_get_main_queue(), {
//UI must be updated on main thread queue
self.updateProgressBar()
})
if self.numImagesLoaded == self.numImagesToLoad {
self.continueLoad()
}
}
UI must be updated on MAIN thread queue!
You also have to use self for accessing pieceImages.

Downloaded picture takes a very long time to be shown on UIImageView

I have two UIImageView objects on my view stored inside an array called imgViews and I try to download images for them asynchronously with this code:
func showPic(positionIndex: Int){
let urlStr = "https://www.friendesque.com/arranged/userpics/amir/1"
let url = NSURL(string: urlStr)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url!, completionHandler: { (data, response, error) -> Void in
if error == nil {
self.imgViews[positionIndex].image = UIImage(data: data)
//self.imgViews[positionIndex].image = UIImage(named: "11665489_10154101221228009_2542754962143804380_n.jpg")
print("Loading Done...")
}
else{
print(error)
}
})
task.resume()
}
and inside my viewDidLoad(), I have
showPic(0)
When I run the code, I see "Loading Done..." immediately which means the picture has been loaded but it takes a very long time (about 1 min) for the UIImageView to actually change to the loaded picture. It's a very small picture (~15K) and it can't be a processing time problem.
I tried loading a resource image (the comment part of the code) instead of the downloaded picture but it's still slow.
I'm really confused. Why is swift so slow at working with images inside a block?
Perhaps when the data task returns it is on a background thread? You will need to switch to the main thread to change a UIImageView. Regardless I would use the UIImageView+AFNetworking category to achieve this. It's simple, well tested and lets you provide a placeholder image to display while it is downloading.
https://github.com/AFNetworking/AFNetworking/blob/master/UIKit%2BAFNetworking/UIImageView%2BAFNetworking.h
to use:
myImageView.setImageWithURL(url!)

Resources