so In my collection view cell I have text and image: This is my code in CollectionViewLayout.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if let content = HomeCollectionViewController.posts[indexPath.item].content {
let spaceForPostContentLabel = NSString(string: content).boundingRect(with: CGSize(width: view.frame.width - 32, height: 120), options: NSStringDrawingOptions.usesFontLeading.union(NSStringDrawingOptions.usesLineFragmentOrigin), attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 15)], context: nil)
if HomeCollectionViewController.posts[indexPath.item].imageURL != nil {
return CGSize(width: view.frame.width, height: spaceForPostContentLabel.height + postImageViewOriginHeight + 168.0)
} else {
return CGSize(width: view.frame.width, height: spaceForPostContentLabel.height + 152.5)
}
} else {
return CGSize(width: view.frame.width, height: 408.5)
}
}
Everything is fine, when it first loaded. But when I scrolled down and scrolled up again, everything is messed up, image is gone and there is a huge blank space where the image should have been existed. Does this have to do with dequeReusableIdentifier?
Note: This error only happen for the first cell, other cell that has image works fine
That's probably happening due to how dequeue works. Even if you have 100 cells, there is a limit for cells loaded at the same time, so after this limit, you start reusing old cells as you scroll.
I already faced the same problem sometimes mainly when using images and the best approach that I found, was to use a cache to the images.
Below I'm posting an example using AlamofireImage to create the cache (but you can use the cache that you prefer, even the builtin cache provided by Swift's library.
import AlamofireImage
let imageCache = AutoPurgingImageCache()
class CustomImageView: UIImageView {
var imageUrlString: String?
func loadImageFromURL(_ urlString: String){
imageUrlString = urlString
let url = URL(string: urlString)
image = nil
if let imageFromCache = imageCache.image(withIdentifier: urlString) {
self.image = imageFromCache
return
}
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if error != nil {
print(error)
return
}
DispatchQueue.main.async(execute: {
let imageToCache = UIImage(data: data!)
if self.imageUrlString == urlString {
self.image = imageToCache
}
imageCache.add(imageToCache!, withIdentifier: urlString)
})
}).resume()
}
}
Basically I'm creating a subclass of UIImageView and adding the image URL as the key to the image that I'm going to keep in the cache. Everytime that I try to load the image from the internet, I check if the image isn't already in the cache, if it is, I set the image to the image in the cache, if not, I load it from the internet asynchronously.
Related
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.
Hi i am making an application in Xcode and using swift for that. I am downloading images from Firebase and show them in the table view. There are some problems with that. But first i will show the code.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! FrontViewCell
cell.contentView.backgroundColor = UIColor.clear
//let whiteRoundedView : UIView = UIView(frame: CGRect(10, 8, self.view.frame.size.width - 20, 149))
let whiteRoundedView: UIView = UIView(frame: CGRect(x: 10, y: 8, width: self.view.frame.width - 20, height: 200))
whiteRoundedView.layer.backgroundColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1.0, 1.0, 1.0, 0.8])
whiteRoundedView.layer.masksToBounds = false
whiteRoundedView.layer.cornerRadius = 2.0
whiteRoundedView.layer.shadowOffset = CGSize(width: -1, height: 1)
whiteRoundedView.layer.shadowOpacity = 0.2
cell.contentView.addSubview(whiteRoundedView)
cell.contentView.sendSubview(toBack: whiteRoundedView)
//cell.categoryImageView.image = catImages[indexPath.row]
//print("Product \(allCats[indexPath.row].name)")
cell.categoryLabel.text = allCats[indexPath.row].name
if let n = allCats[indexPath.row].name{
con?.storage?.reference(withPath: "categories/\(n).png").data(withMaxSize: 10 * 1024 * 1024, completion: {
data, error in
if error == nil{
let im = UIImage(data: data!)
cell.categoryImageView.image = im
cell.layoutSubviews()
}
else{
print("Error Downloading Image \(error?.localizedDescription)")
}
})
}
return cell
}
So above is the code to set the images to an imageView in the cell.
Problems
When i scroll down and then scroll up again, the images are different in the same cells.
The tableview scrolling is very laggy.
These are the problems. Please let me know how can i solve this?
I know of a library SDWebImage but i don't know how to download Firebase image with that library. Please help me through this problem. I am very exhausted by this problem. I have been trying to solve it for the last 20 hours without sleep but could not. Please let me know what i am doing wrong and how should i fix that. Thanks.
TableView is laggy because you are redownloading images all the time.
This is a caching issue.
As for the images being different in the same cell, you can change this just by resseting the image to nil, because cells are being reused, they are using a previous image, while the new one downloads.
But both of these issues would be fixed if you were to use some caching framework, for example, probably the best one out there is SDWebImage.
If you don't wanna use a library for this. Here is the most basic implementation of caching images.
let imageCache = NSCache<AnyObject, AnyObject>()
extension UIImageView {
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = nil
//check cache for image
if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = cachedImage
return
}
//otherwise start the download
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
//there was an error with the download
if error != nil {
print(error ?? "")
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as AnyObject)
self.image = downloadedImage
}
})
}).resume()
}
}
Usage:
cell.categoryImageView.loadImageUsingCacheWithUrlString("your firebase url string")
EDIT: Yes, you can use this to download images that are stored in Firebase.
EDIT: This code will solve your issues, but memory management is not considered here, for a serious production app I would suggest looking into libraries dedicated to image caching.
EDIT: I just noticed that there is proper info on Firebase documentation , showing how it works with SDWebImage. Check it out: SDWebImage + Firebase
I'm currently reading images from my firebase storage - which works fine.
I have set up a caching to read images from the cache when it has been read from the storage:
// Storage.imageCache.object(forKey: post.imageUrl as NSString)
static func getImage(with url: String, completionHandler: #escaping (UIImage) -> ())
{
if let image = imageCache.object(forKey: url as NSString)
{
print("CACHE: Unable to read image from CACHE ")
completionHandler(image)
}
else
{
let ref = FIRStorage.storage().reference(forURL: url)
ref.data(withMaxSize: 2 * 1024 * 1024)
{
(data, error) in
if let error = error
{
print("STORAGE: Unable to read image from storage \(error)")
}
else if let data = data
{
print("STORAGE: Image read from storage")
if let image = UIImage(data: data)
{
// Caches the image
Storage.imageCache.setObject(image, forKey: url as NSString)
completionHandler(image)
}
}
}
}
}
}
But its not working. It seems to not work at all as well, I don't have the message ' print("CACHE: Unable to read image from CACHE ")
' being displayed on my console but the print ' print("STORAGE: Image read from storage")
'
Do you know how this can be achieved by any chance please?
Thanks a lot for your time!
---EDIT --
I call the image in table cell view from firebase storage then as:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.feedTableView.dequeueReusableCell(withIdentifier: "MessageCell")! as UITableViewCell
let imageView = cell.viewWithTag(1) as! UIImageView
let titleLabel = cell.viewWithTag(2) as! UILabel
let linkLabel = cell.viewWithTag(3) as! UILabel
titleLabel.text = posts[indexPath.row].title
titleLabel.numberOfLines = 0
linkLabel.text = posts[indexPath.row].link
linkLabel.numberOfLines = 0
Storage.getImage(with: posts[indexPath.row].imageUrl){
postPic in
imageView.image = postPic
}
return cell
}
You can realize caching images with Kingfisher for example. And works better. link
How to use: Add link to your image from storage to database item node. Like this:
Then just use it to present and cache image.
Example:
let imageView = UIImageView(frame: frame) // init with frame for example
imageView.kf.setImage(with: <urlForYourImageFromFireBase>) //Using kf for caching images
Hope it helps
I am new in IOS development using Swift. I created 1 UITableView and displaying images after downloading data. But it is not smooth and some time images are displaying in wrong place when i am scrolling.
I am using AlamofireImage library for image downloading and displaying. Is there any fast library?
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:HomePageCell = tableView.dequeueReusableCell(withIdentifier: "HomePage", for: indexPath) as! HomePageCell
cell.configure( homeData[0], row: indexPath, screenSize: screenSize,
hometableview: self.homeTableView);
return cell
}
import UIKit
import Alamofire
import AlamofireImage
class HomePageCell: UITableViewCell {
#IBOutlet weak var bannerImage: UIImageView!
func configure(_ homeData: HomeRequest, row: IndexPath, screenSize: CGRect, hometableview: UITableView) {
let callData = homeData.banner_lead_stories[(row as NSIndexPath).row]
let url = Constants.TEMP_IMAGE_API_URL + callData.lead_story[0].bg_image_mobile;
if( !callData.lead_story[0].bg_image_mobile.isEmpty ) {
if bannerImage?.image == nil {
let range = url.range(of: "?", options: .backwards)?.lowerBound
let u = url.substring(to: range!)
Alamofire.request(u).responseImage { response in
debugPrint(response)
//print(response.request)
// print(response.response)
// debugPrint(response.result)
if let image = response.result.value {
// print("image downloaded: \(image)")
self.bannerImage.image = image;
self.bannerImage.frame = CGRect(x: 0, y: 0, width: Int(screenSize.width), height: Int(screenSize.width/1.4))
}
}
}
} else {
self.bannerImage.image = nil;
}
}
}
It can be not smooth, because you need to cache your images and make a downloading process not in main thread(read about GCD).
For caching you can go two ways (atleast):
1) Make your own array of images where they will be cached
2) Use KingFisher for example click. It will cache your images.
For example:
yourImageView.kf.setImage(with: URL) // next time, when you will use image with this URL, it will be taken from cache.
Hope it helps
You can use SDWebImage for downloading the image array and add a placeholder image for the time being in imageView. this is function
public func sd_setImageWithURL(url: NSURL!, placeholderImage placeholder: UIImage!)
and it is as easy to use as
myImageView.sd_setImageWithURL(NSURL(string:image), placeholderImage:UIImage(named:"qwerty"))
make sure to reset you imageView in tableView delegate cellforRowAtIndexpath method by setting imageview image to nil
myImageView.image = nil
//now set image in imageView
myImageView.sd_setImageWithURL(NSURL(string:image), placeholderImage:UIImage(named:"qwerty"))
this avoids the image duplicating and weird behave of images as imageview of every cell is being reset before reusing.
Github link -> https://github.com/rs/SDWebImage
You have to use multithreading.Only UI is set in main thread, downloading image in background is in another thread.By this way you can solve your problem.
Try SDWebImage library it will save images in catch automatically and your tableView will work smoothly.
Github link -> https://github.com/rs/SDWebImage
Install pod:
platform :ios, '7.0'
pod 'SDWebImage', '~>3.8'
Just import SDWebImage like:
#import SDWebImage
And use like this:
imageView.sd_setImage(with: URL(string: "http://www.example.com/path/to/image.jpg"), placeholderImage: UIImage(named: "placeholder.png"))
I used it in many live projects and it works like a charm :)
Use this extension to cache your images, and also don't forget to update any UI on the main thread.
let imageCache = NSCache<NSString, UIImage>()
extension UIImageView {
func loadImageUsingCacheWithURLString(_ URLString: String, placeHolder: UIImage?) {
self.image = nil
if let cachedImage = imageCache.object(forKey: NSString(string: URLString)) {
self.image = cachedImage
return
}
if let url = URL(string: URLString) {
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
//print("RESPONSE FROM API: \(response)")
if error != nil {
print("ERROR LOADING IMAGES FROM URL: \(error)")
DispatchQueue.main.async {
self.image = placeHolder
}
return
}
DispatchQueue.main.async {
if let data = data {
if let downloadedImage = UIImage(data: data) {
imageCache.setObject(downloadedImage, forKey: NSString(string: URLString))
self.image = downloadedImage
}
}
}
}).resume()
}
}
}
When I try to get photos using a URL from a local gallery I add them to a tableView using ALAssetsLibrary()
My code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: AllPhotosTableViewCell!
cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! AllPhotosTableViewCell
let url = array[indexPath.section]["photos"] as! [AnyObject]
if let image = cachedImages[url[indexPath.row] as! String] {
cell.imageViewImg.image = imageResize(image, sizeChange: CGSize(width: 50, height: 50))
} else {
let nsURL = NSURL(string: url[indexPath.row] as! String)!
var loadError: NSError?
asset.assetForURL(nsURL, resultBlock: { (asset) -> Void in
if let ast = asset {
let image = UIImage(CGImage: ast.defaultRepresentation().fullResolutionImage().takeUnretainedValue())
self.cachedImages[url[indexPath.row] as! String] = image
dispatch_async(dispatch_get_main_queue(), {
cell.imageViewImg.image = self.imageResize(image, sizeChange: CGSize(width: 50, height: 50))
})
}
}, failureBlock: {(error) -> Void in
loadError = error
})
}
return cell
}
I try to minimize the size of photos, it helps a little but it doesn't solve the problem if I need to use many photos (10 or more...)
What is self.cachedImages. If that's a dictionary then your code is holding ALL of your images in memory, and as you scroll your memory footprint will grow and grow. The simplest way to fix that would be to get rid of the cache dictionary entirely and load the images from the asset library every time, but that might be slow.
If you are scaling your images to display them in a table view then load the asset, open a graphics context at the target size, render the image at your thumbnail size, and cache the thumbnail image.
You might also look at using NSCache, which acts like a dictionary but manages flushing objects when memory gets low.