I define var dictImages = NSMutableDictionary() to store downloaded images and to not load more than one time, but when I remove row from tableView in Firebase, images are sliding. I figured out that problem is in dictImages. The only way to prevent that error is to write
dictImages.removeAllObjects()
before downloading images, but in this case there is no meaning of storing images and it is not efficient. I need to figure out previously stored item and remove it.
This code is written in cellForRowAt function of tableView :
if let image = self.dictImages.object(forKey: indexPath)
{
cell?.newsImg.image = image as? UIImage
}
else{
storageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print(error)
} else {
if let data = data
{
let image = UIImage(data: data)
self.dictImages.setObject(image, forKey: indexPath as NSCopying)
cell?.newsImg.image = image
}
}
}
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.
I'm develop an iOS app that upload a list of images to firebase storage, I need to put a progress view inside each cell of uicollectionview (which show each image in datasource) to indicate percentage of uploading this image to firebase storage.
I try this code:
func Upload(imageView: UIImageView)
{
// Data in memory
var data = Data()
data = UIImageJPEGRepresentation(self.pickedImage, 0.8)!
// Create a root reference
let storageRef = Storage.storage().reference()
let imageName = NSUUID().uuidString
// Create a reference to the file you want to upload
let imageRef = storageRef.child("images/\(imageName).jpg")
// Upload the file to the path "images/rivers.jpg"
let uploadTask = imageRef.putData(data, metadata: nil) { (metadata, error) in
guard let metadata = metadata else {
// Uh-oh, an error occurred!
return
}
}
let observer = uploadTask.observe(.progress) { snapshot in
print("snapshot.progress",snapshot.progress)
}
uploadTask.observe(.progress) { snapshot in
let percentComplete = 100.0 * Double(snapshot.progress!.completedUnitCount)
/ Double(snapshot.progress!.totalUnitCount)
print("percentComplete",percentComplete)
let placeholderImage = UIImage(named: "la")
imageView.sd_addActivityIndicator()
imageView.sd_showActivityIndicatorView()
imageView.sd_setImage(with: imageRef, placeholderImage: placeholderImage)
}
uploadTask.observe(.success) { snapshot in
self.url = imageName
print(imageName)
print("success")
imageView.sd_removeActivityIndicator()
}
uploadTask.observe(.failure) { snapshot in
print("Failuer")
}
}
and:
arrayOfImages?[(arrayOfImages?.count)! - 1]?.Upload(imageView: cell.workImage)
but I'm couldn't to get that because reuse of cell in UICollectionView, How can I get that?
1- Create a class and paste this code inside a function
2- Make progress property inside that class and regularly update it inside the progress completion and another property for the image
3- Create an instance array inside the class and init it with the image and start the upload
4- Inside cellForRowAt get the current progress value for every index , that's how you avoid cell reusing and uploading of image that be uploaded or currently uploading
//
It would be something like this
class Upload {
var currentState:State = .none
var progress:Float = 0
var img:UIImage
init(image:UIImage) {
img = image
}
func start () {
currentState = .uploading
///
let observer = uploadTask.observe(.progress) { snapshot in
self.progress = snapshot.progress
}
///
uploadTask.observe(.success) { snapshot in
currentState = .uploaded
}
//
}
}
enum State {
case uploading,uploaded,none
}
//
var arr = [Upload]()
for i in 0..<numberOfImages {
let img = UIImage(named: "\(i).png") // or get it anyWay
let up = Upload(image: img)
up.start() // start it in cellForRowAt if item state is none
arr.append(up)
}
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 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 try to store array of UIImage, it used to work but somehow all of sudden it refused to work. I wrote this test example to check if it's stored properly and I guess the problem is somewhere here
let images = NSKeyedArchiver.archivedDataWithRootObject(self.globalImageArray)
NSUserDefaults.standardUserDefaults().setObject(images, forKey: "morningImages")
NSUserDefaults.standardUserDefaults().synchronize()
println("Images saved")
let images2 = NSUserDefaults.standardUserDefaults().objectForKey("morningImages") as? NSData
let imagesArray = NSKeyedUnarchiver.unarchiveObjectWithData(images2!) as! NSArray
var testArray = imagesArray as! [UIImage]
println("Check if images are loaded " + "\(testArray.count)")
The testArray.count is equal to zero which means it either fails to save them properly or fails to retrieve them.
I tried printing images2 and it does contain data, but printing the next value which is imagesArray leads to the result equals to "()" Guess the problem is with this line:
let imagesArray = NSKeyedUnarchiver.unarchiveObjectWithData(images2!) as! NSArray
Thanks for help in advance.
As others have pointed out, this really isn't the best use case for NSUserDefaults. This should be very simple to do by writing to and reading from files on disk.
Here's how I'd do it, and it's less code than saving to NSUserDefaults!
NSKeyedArchiver.archiveRootObject(self.globalImageArray, toFile: "/path/to/archive")
println("Images saved")
if let testArray = NSKeyedUnarchiver.unarchiveObjectWithFile("/path/to/archive") as? [UIImage] {
println("Check if images are loaded " + "\(testArray.count)")
} else {
println("Failed to load images.")
}
EDIT As it turns out this doesn't work on cached images (e.g. any UIImage loaded with either of the UIImage(named:) variants because the cached images don't seem to get serialized to disk. So, while the above works for not cached UIImages, the following works regardless of the image's cached status.
// Save the raw data of each image
if NSKeyedArchiver.archiveRootObject(imageArray.map { UIImagePNGRepresentation($0) }, toFile: archivePath) {
println("\(imageArray.count) Images saved")
} else {
println("failed to save images")
}
// Load the raw data
if let dataArray = NSKeyedUnarchiver.unarchiveObjectWithFile(archivePath) as? [NSData] {
// Transform the data items to UIImage items
let testArray = dataArray.map { UIImage(data: $0)! }
println("\(testArray.count) images loaded.")
} else {
println("Failed to load images.")
}