Download images using Alamofire and display in UITableViewCell - ios

I have these two functions:
func callAlamo(url: String){
Alamofire.request(url).responseJSON(completionHandler: {
response in
self.parseData(JSONData: response.data!)
})
}
func parseData(JSONData: Data) {
do {
var readableJSON = try JSONSerialization.jsonObject(with: JSONData, options: .mutableContainers) as! JSONStandard
if let tracks = readableJSON["tracks"] as? JSONStandard{
if let items = tracks["items"] as? [JSONStandard]{
for i in 0..<items.count{
let item = items[i]
let name = item["name"] as! String
if let album = item["album"] as? JSONStandard{
if let images = album["images"] as? [JSONStandard]{
let imageData = images[0]
let mainImageUrl = URL(string: imageData["url"] as! String)
let mainImageData = NSData(contentsOf: mainImageUrl!)
let mainImage = UIImage(data: mainImageData as! Data)
posts.append(post.init(image: mainImage, name: name))
self.tableView.reloadData()
}
}
}
}
}
}
catch{
print(error)
}
}
That retrieve song information from Spotify using alamofire. These functions are set up inside of a UITableViewController class. I have a separate UITableViewCell class set up in the same file where I set up this image:
let albumCoverImageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
How would I use get the retrieved information from the two functions into that image? Because I cannot access the functions from inside the image imitation.

Use "SDWebImage" library for downloading images asynchronously.
Refer below link :-
https://github.com/rs/SDWebImage
Usage :-
Objective-C:
#import <SDWebImage/UIImageView+WebCache.h>
[imageView sd_setImageWithURL:[NSURL
URLWithString:imageURL]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
Swift:
import SDWebImage
imageView.sd_setImage(with: URL(string: imageURL), placeholderImage: UIImage(named: "placeholder.png"))

var imgFinal = [String:UIImage]()
Store your images in "imgFinal" array in func parseData():
self.imgArrList[i] = imageData["url"]
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! CategoryTableViewCell
load_image((imgArrList[indexPath.row] as? String)!, imageview: cell.titleImage, indVal: indexPath.row)
return cell
}
Add this method to download image:
func load_image(link:String, imageview:UIImageView, indVal:Int)
{
let url:NSURL = NSURL(string: link)!
let session = NSURLSession.sharedSession()
let request = NSMutableURLRequest(URL: url)
request.timeoutInterval = 10
let task = session.dataTaskWithRequest(request) {
(
let data, let response, let error) in
guard let _:NSData = data, let _:NSURLResponse = response where error == nil else {
return
}
var image = UIImage(data: data!)
if (image != nil)
{
func set_image()
{
//self.images_cache[link] = image
self.imgFinal[link] = image
imageview.image = image
}
dispatch_async(dispatch_get_main_queue(), set_image)
}
}
task.resume()
}

Related

TableView with labels, images, gifs and video hangs / gets stuck incorrect while fetch from firestore in iOS, Swift

I have tableview with label, imageView (for image, gif & video thumbnail). I am sure that doing something wrong and I can't handle its completion handler due to which the app is hanged and gets stuck for a long time.
My model is like,
struct PostiisCollection {
var id :String?
var userID: String?
var leadDetails : NSDictionary?
var company: NSDictionary?
var content: String?
init(Doc: DocumentSnapshot) {
self.id = Doc.documentID
self.userID = Doc.get("userID") as? String ?? ""
self.leadDetails = Doc.get("postiiDetails") as? NSDictionary
self.company = Doc.get("company") as? NSDictionary
self.content = Doc.get("content") as? String ?? ""
}
}
I wrote in my view controller for fetch this,
var postiisCollectionDetails = [PostiisCollection]()
override func viewDidLoad() {
super.viewDidLoad()
let docRef = Firestore.firestore().collection("PostiisCollection").whereField("accessType", isEqualTo: "all_access")
docRef.getDocuments { (querysnapshot, error) in
if let doc = querysnapshot?.documents, !doc.isEmpty {
print("Document is present.")
for document in querysnapshot!.documents {
_ = document.documentID
if let compCode = document.get("company") as? NSDictionary {
do {
let jsonData = try JSONSerialization.data(withJSONObject: compCode)
let companyPost: Company = try! JSONDecoder().decode(Company.self, from: jsonData)
if companyPost.companyCode == AuthService.instance.companyId ?? ""{
print(AuthService.instance.companyId ?? "")
if (document.get("postiiDetails") as? NSDictionary) != nil {
let commentItem = PostiisCollection(Doc: document)
self.postiisCollectionDetails.append(commentItem)
}
}
} catch {
print(error.localizedDescription)
}
DispatchQueue.main.async {
self.tableView.isHidden = false
self.tableView.reloadData()
}
}
}
}
}
}
I need to check for the index path with image view is either image or gif or video with different parameters, I tried with tableview delegate and datasource method by,
extension AllAccessPostiiVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postiisCollectionDetails.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AllAccessCell", for: indexPath)
let label1 = cell.viewWithTag(1) as? UILabel
let imagePointer = cell.viewWithTag(3) as? UIImageView
let getGif = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "gif") as? NSArray
let getPhoto = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "photo") as? NSArray
let getVideo = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "video") as? NSArray
label1?.text = "\(arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "title"))"
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
print(arrGif[0])
let gifURL : String = "\(arrGif[0])"
let imageURL = UIImage.gifImageWithURL(gifURL)
imagePointer?.image = imageURL
playButton?.isHidden = true
}
if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
print(arrPhoto[0])
let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
storageRef.downloadURL(completion: { (url, error) in
do {
let data = try Data(contentsOf: url!)
let image = UIImage(data: data as Data)
DispatchQueue.main.async {
imagePointer?.image = image
playButton?.isHidden = true
}
} catch {
print(error)
}
})
}
if getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let videoURL = URL(string: arrVideo[0])
let asset = AVAsset(url:videoURL!)
if let videoThumbnail = asset.videoThumbnail{
SVProgressHUD.dismiss()
imagePointer?.image = videoThumbnail
playButton?.isHidden = false
}
}
}
}
If I run, the app hangs in this page and data load time is getting more, some cases the preview image is wrongly displayed and not able to handle its completion
As others have mentioned in the comments, you are better of not performing the background loading in cellFroRowAtIndexPath.
Instead, it's better practice to add a new method fetchData(), where you perform all the server interaction.
So for example:
// Add instance variables for fast access to data
private var images = [UIImage]()
private var thumbnails = [UIImage]()
private func fetchData(completion: ()->()) {
// Load storage URLs
var storageURLs = ...
// Load data from firebase
let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
storageRef.downloadURL(completion: { (url, error) in
// Parse data and store resulting image in image array
...
// Call completion handler to indicate that loading has finished
completion()
})
}
Now you can call fetchData() whenever you would like to refresh data and call tableview.reloadData() within the completion handler. That of course must be done on the main thread.
This approach simplifies your cellForRowAtIndexPath method.
There you can just say:
imagePointer?.image = ...Correct image from image array...
Without any background loading.
I suggest using below lightweight extension for image downloading from URL
using NSCache
extension UIImageView {
func downloadImage(urlString: String, success: ((_ image: UIImage?) -> Void)? = nil, failure: ((String) -> Void)? = nil) {
let imageCache = NSCache<NSString, UIImage>()
DispatchQueue.main.async {[weak self] in
self?.image = nil
}
if let image = imageCache.object(forKey: urlString as NSString) {
DispatchQueue.main.async {[weak self] in
self?.image = image
}
success?(image)
} else {
guard let url = URL(string: urlString) else {
print("failed to create url")
return
}
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
// response received, now switch back to main queue
DispatchQueue.main.async {[weak self] in
if let error = error {
failure?(error.localizedDescription)
}
else if let data = data, let image = UIImage(data: data) {
imageCache.setObject(image, forKey: url.absoluteString as NSString)
self?.image = image
success?(image)
} else {
failure?("Image not available")
}
}
}
task.resume()
}
}
}
Usage:
let path = "https://i.stack.imgur.com/o5YNI.jpg"
let imageView = UIImageView() // your imageView, which will download image
imageView.downloadImage(urlString: path)
No need to put imageView.downloadImage(urlString: path) in mainQueue, its handled in extension
In your case:
You can implement following logic in cellForRowAt method
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
let urlString : String = "\(arrGif[0])"
let image = UIImage.gifImageWithURL(urlString)
imagePointer?.image = image
playButton?.isHidden = true
}
else if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
let urlString = Storage.storage().reference(forURL: arrPhoto[0])
imagePointer?.downloadImage(urlString: urlString)
playButton?.isHidden = true
}
elseif getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let urlString = arrVideo[0]
imagePointer?.downloadImage(urlString: urlString)
playButton?.isHidden = false
}
If you have one imageView to reload in tableView for photo, video and gif. then use one image array to store it prior before reloading. So that your main issue of hang or stuck will be resolved. Here the main issue is each time in table view cell collection data is being called and checked while scrolling.
Now I suggest to get all photo, gifs and video (thumbnail) as one single array prior to table view reload try this,
var cacheImages = [UIImage]()
private func fetchData(completionBlock: () -> ()) {
for (index, _) in postiisCollectionDetails.enumerated() {
let getGif = postiisCollectionDetails[index].leadDetails?.value(forKey: "gif") as? NSArray
let getPhoto = postiisCollectionDetails[index].leadDetails?.value(forKey: "photo") as? NSArray
let getVideo = postiisCollectionDetails[index].leadDetails?.value(forKey: "video") as? NSArray
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
let gifURL : String = "\(arrGif[0])"
self.randomList.append(gifURL)
/////---------------------------
let imageURL = UIImage.gifImageWithURL(gifURL)
self.cacheImages.append(imageURL!)
//////=================
}
else if getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let videoURL: String = "\(arrVideo[0])"
let videoUrl = URL(string: arrVideo[0])
let asset = AVAsset(url:videoUrl!)
if let videoThumbnail = asset.videoThumbnail{
////--------------
self.cacheImages.append(videoThumbnail)
//-----------
}
self.randomList.append(videoURL)
}else if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
let photoURL : String = "\(arrPhoto[0])"
/////---------------------------
let url = URL(string: photoURL)
let data = try? Data(contentsOf: url!)
if let imageData = data {
let image = UIImage(data: imageData)
if image != nil {
self.cacheImages.append(image!)
}
else {
let defaultImage: UIImage = UIImage(named:"edit-user-80")!
self.cacheImages.append(defaultImage)
}
}
//////=================
}
else {
//-----------------
let defaultImage: UIImage = UIImage(named:"edit-user-80")!
self.cacheImages.append(defaultImage)
//--------------------
}
}
completionBlock()
}
After getting all UIImage as array where loop is being called. Now you call this function inside your viewDidLoad. So after all values in images fetched then try to call tableView like this,
override func viewDidLoad() {
self.fetchData {
DispatchQueue.main.async
self.tableView.reloadData()
}
}
}
Now atlast, you may use SDWebImage or any other background image class or download method to call those in tableView cellforRow method,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// your cell idetifier & other stuffs
if getVideo != nil {
imagePointer?.image = cacheImages[indexPath.row]
playButton?.isHidden = false
}else {
imagePointer?.image = cacheImages[indexPath.row]
// or get photo with string via SdWebImage
// imagePointer?.sd_setImage(with: URL(string: photoURL), placeholderImage: UIImage(named: "edit-user-80"))
playButton?.isHidden = true
}
return cell
}
You're handling data in a totally wrong manner. Data(contentsOf: url!) - This is wrong. You should chache the images and should download it to directory. When you convert something into data it takes place into the memory(ram) and it is not good idea when playing with large files. You should use SDWebImage kind of library to set images to imageview.
Second thing if let videoThumbnail = asset.videoThumbnail - This is also wrong. Why you're creating assets and then getting thumbnail from it? You should have separate URL for the thumbnail image for your all videos in the response of the API and then again you can use SDWebImage to load that thumbnail.
You can use SDWebImage for gif as well.
Alternative of SDWebImage is Kingfisher. Just go through both libraries and use whatever suitable for you.

How to convert a JSON Image-String (not url) to a UIImage for viewing

Hey so I've literally been trying to solve this for a day now. So I'm desperate for an answer. What I'm trying to do its actually quite simply I have a JSON array of images in the form of a string (I think). They are not url's the image/string comes in JSON like this "GoodLogo.png" the problems is usual image from JSON come in url format. But this just says png & jpg
JSON Code I get Back:
(
{
id = 1;
logo = "scriptslogo.png";
name = "scripts";
},
{
id = 2;
logo = "altrlogo.jpg";
name = "Altr";
},
{
id = 3;
logo = "bostonlogo.jpeg";
name = "boston";
},
)
I have a variable which is an array of strings. That I put the JSON Data in.
Code:
var imageList = [String]()
let string = "https://some.url/api"
let url = NSURL(string: string)
let request = NSMutableURLRequest(url: url! as URL)
request.httpMethod = "GET"
let session = URLSession.shared
let tache = session.dataTask(with: request as URLRequest) { (data, response, error) -> Void in
if let jsonObj = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! NSArray {
let logo = jsonObj.value(forKey: "logo") as? NSArray
self.imageList = logo as! [String]
DispatchQueue.main.async {
self.imageTableView.reloadData()
}
}
}
tache.resume()
Then all I do is try and populate the tableview but every single way that I've tried below either crashes application or doesn't show anything.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == imageTableView {
cell = imageTableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath) as! imageCell
cell.cellImage.image = imageList[indexPath.row]
// Iv'e tried this which I don't no if it even applies to what I'm trying to do
if let imageData = Data(base64Encoded: imageList[indexPath.row], options: .ignoreUnknownCharacters) {
let image = UIImage(data: imageData)
cell.cellImage.image = image
}
}
return cell
Try This
let url = URL(string: "http://test.com/images/\(imageList[indexPath.row])")
let data = try? Data(contentsOf: url)
if let imageData = data {
cell.cellImage.image = UIImage(data: data)
}

Existing CollectionView Image changes on scroll

I have looked at a lot of questions and I do not believe this is a result of reusing a cell as the new cells image is correct, but and existing cells image is incorrect and used to be correct. I'll post the images first so the issue is easier to understand.
I have a collectionView of image cells (similar to Instagrams user page). I'm fetching all of the data from Firebase. I get the first 12 posts on the initial loading of the screen. However, if you scroll down quickly an EXISTING cells image changes to a newly fetched image. I'm not sure why this is happening... Maybe it's a caching issue? The issue only occurs the first time you load the screen. I've tried setting the images to nil like this:
override func prepareForReuse() {
super.prepareForReuse()
self.imageView.image = UIImage()
}
This didn't help the issue though.
Here is my cellForItemAt:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "imageCell", for: indexPath) as! ImageCell
cell.indexPath = indexPath
cell.imageView.downloadImage(from: currentTablePosts[indexPath.row].pathToImage)
cell.layer.borderWidth = 1
cell.layer.borderColor = UIColor.black.cgColor
return cell
}
Image downloading and caching:
let imageCache = NSCache<NSString, UIImage>()
extension UIImageView {
func downloadImage(from imgURL: String!) {
let url = URLRequest(url: URL(string: imgURL)!)
// 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
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
// user an alert to display the error
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
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()
}
}
Firebase Queries:
static func getInitialTablesPosts(tableNumber: String) {
tableReference.child(tableNumber).queryLimited(toLast: 12).observeSingleEvent(of: .value, with: { snap in
for child in snap.children {
let child = child as? DataSnapshot
if let post = child?.value as? [String: AnyObject] {
let posst = Post()
if let author = post["author"] as? String, let likes = post["likes"] as? Int, let pathToImage = post["pathToImage"] as? String, let postID = post["postID"] as? String, let postDescription = post["postDescription"] as? String, let timestamp = post["timestamp"] as? Double, let category = post["category"] as? String, let table = post["group"] as? String, let userID = post["userID"] as? String, let numberOfComments = post["numberOfComments"] as? Int, let region = post["region"] as? String {
posst.author = author
posst.likes = likes
posst.pathToImage = pathToImage
posst.postID = postID
posst.userID = userID
posst.fancyPostDescription = Helper.createAttributedString(author: author, postText: postDescription)
posst.postDescription = author + ": " + postDescription
posst.timestamp = timestamp
posst.table = table
posst.region = region
posst.category = category
posst.numberOfComments = numberOfComments
posst.userWhoPostedLabel = Helper.createAttributedPostLabel(username: author, table: table, region: region, category: category)
if let people = post["peopleWhoLike"] as? [String: AnyObject] {
for(_, person) in people {
posst.peopleWhoLike.append(person as! String)
}
}
currentTablePosts.insert(posst, at: 0)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTableCollectionView"), object: nil)
} // end of if let
}
}
})
tableReference.removeAllObservers()
}
static func getMoreTablePosts(tableNumber: String, lastVisibleKey: String) {
print("FIRED...")
let currentNumberOfPosts = currentTablePosts.count
print("Number of posts before fetiching ", currentNumberOfPosts)
print("Oldest post key ", oldestTableKeys[tableNumber] ?? "not set yet", "***********")
tableReference.child(tableNumber).queryOrderedByKey().queryEnding(atValue: lastVisibleKey).queryLimited(toLast: 12).observeSingleEvent(of: .value, with: { snap in
for child in snap.children {
let child = child as? DataSnapshot
if let post = child?.value as? [String: AnyObject] {
if let id = post["postID"] as? String {
if id == lastVisibleKey {
return
}
}
let posst = Post()
if let author = post["author"] as? String, let likes = post["likes"] as? Int, let pathToImage = post["pathToImage"] as? String, let postID = post["postID"] as? String, let postDescription = post["postDescription"] as? String, let timestamp = post["timestamp"] as? Double, let category = post["category"] as? String, let table = post["group"] as? String, let userID = post["userID"] as? String, let numberOfComments = post["numberOfComments"] as? Int, let region = post["region"] as? String {
posst.author = author
posst.likes = likes
posst.pathToImage = pathToImage
posst.postID = postID
posst.userID = userID
posst.fancyPostDescription = Helper.createAttributedString(author: author, postText: postDescription)
posst.postDescription = author + ": " + postDescription
posst.timestamp = timestamp
posst.table = table
posst.region = region
posst.category = category
posst.numberOfComments = numberOfComments
posst.userWhoPostedLabel = Helper.createAttributedPostLabel(username: author, table: table, region: region, category: category)
if let people = post["peopleWhoLike"] as? [String: AnyObject] {
for(_, person) in people {
posst.peopleWhoLike.append(person as! String)
}
}
currentTablePosts.insert(posst, at: currentNumberOfPosts)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTableCollectionView"), object: nil)
if let oldestTableKey = oldestTableKeys[tableNumber] {
if postID == oldestTableKey {
print("returning")
print("number of posts on return \(currentTablePosts.count)")
return
}
}
} // end if let
}
}
})
tableReference.removeAllObservers()
}
* UPDATE *
The image caching used here still had some issues in my experience. I have since moved on to using Kingfisher which is extremely easy to setup and use.
* OLD SOLUTION *
Found a solution based on the answer suggested in the comments.
I modified the extension I am using to cache my images. Although in the future I think I will subclass UIImageView. Here is the modified version of my code.
import UIKit
let userImageCache = NSCache<NSString, UIImage>()
let imageCache = NSCache<AnyObject, AnyObject>()
var imageURLString: String?
extension UIImageView {
public func imageFromServerURL(urlString: String, collectionView: UICollectionView, indexpath : IndexPath) {
imageURLString = urlString
if 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, completionHandler: { (data, response, error) in
if error != nil{
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
return
}
DispatchQueue.main.async(execute: {
if let imgaeToCache = UIImage(data: data!){
if imageURLString == urlString {
self.image = imgaeToCache
}
imageCache.setObject(imgaeToCache, forKey: urlString as AnyObject)// calls when scrolling
collectionView.reloadItems(at: [indexpath])
}
})
}) .resume()
}
}
func downloadImage(from imgURL: String!) {
let url = URLRequest(url: URL(string: imgURL)!)
// 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 AnyObject) as? UIImage {
self.image = imageToCache
return
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
// user an alert to display the error
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
return
}
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
imageCache.setObject(imageToCache!, forKey: imgURL! as AnyObject)
self.image = imageToCache
}
}
task.resume()
}
func downloadUserImage(from imgURL: String!) {
let url = URLRequest(url: URL(string: imgURL)!)
// 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 = userImageCache.object(forKey: imgURL! as NSString) {
self.image = imageToCache
return
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
// user an alert to display the error
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
userImageCache.setObject(imageToCache!, forKey: imgURL! as NSString)
self.image = imageToCache
}
}
task.resume()
}
}
I created the first method to cache the images for the collectionView and the other methods are still used for tableViews. The cache was also changed from let imageCache = NSCache<NSString, UIImage>() to let imageCache = NSCache<AnyObject, AnyObject>()

the table view is not being updated after download the data

First of all, to avoid my bad English, I uploaded a video showing my problem
http://www.mediafire.com/download/j6krsa274o80ik9/Screen_Recording.mov
Second, I have a UITableViewController, that uses a remote API to download data. the data contains many image URLs, my first problem is that the tableView is not being updated even though i am doing .reloadData() function
my second problem is that in the function:
tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
I download the images, which I had got their urls from the first call, then eventhing works good but I can't see the image unless I clicked on the row
Please see the video, it is easier to understand
Here is my code: (I gave you the full code of my UITableView because, it is simple, and because it has two functions, and they are the ones that making me problems)
class Physicst: NSObject {
let image : String
var imageData: UIImage?
let name : NSString
init(image: String, name: NSString) {
self.image = image
self.name = name
}
}
class PhysicistTableViewController: UITableViewController {
var physicsts : [Physicst]?
#IBOutlet var physicstsTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
loadDataFromDBPedia()
}
func loadDataFromDBPedia() {
let session = NSURLSession.sharedSession()
var url = "http://dbpedia.org/sparql/"
let query = "http://dbpedia.org&query=select * {?o dbo:thumbnail ?p . ?o dbo:award dbr:Nobel_Prize_in_Physics} limit 10"
url = url + "?default-graph-uri=" + query.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())!
url = url + "&format=JSON&CXML_redir_for_subjs=121&CXML_redir_for_hrefs=&timeout=30000&debug=on"
let request = NSMutableURLRequest(URL: NSURL(string: url)!)
let task = session.dataTaskWithRequest(request, completionHandler: {(data, response ,error) in
if let error = error {
print ("\(error)")
}
if let response = response {
let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode
print("Status code = \(statusCode)")
}
if let data = data {
do {
let jsonResponse = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions())
let binding = jsonResponse["results"]!!["bindings"] as! NSArray
for oneBinding in binding {
let name = oneBinding["o"]!!["value"] as! NSString
var image = oneBinding["p"]!!["value"] as! String
image = image.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!
let physicst = Physicst(image: image, name: name)
if self.physicsts == nil {
self.physicsts = [Physicst]()
}
self.physicsts!.append(physicst)
}
self.physicstsTableView.reloadData()
}catch _ {
print ("not well json-formatted response")
}
}
})
task.resume()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.physicsts == nil {
return 0
}else {
return self.physicsts!.count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("physicstCell")! as UITableViewCell
let row = indexPath.row
let physicst = self.physicsts![row]
cell.textLabel?.text = physicst.name as String
if (physicst.imageData == nil) {
let session = NSURLSession.sharedSession()
let url = NSURL(string: physicst.image as String)
if let url = url {
let request = NSMutableURLRequest(URL: url)
let task = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
if let data = data {
let imageData = UIImage(data: data)
cell.imageView?.image = imageData
physicst.imageData = imageData
self.physicstsTableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
}
})
task.resume()
}else {
print ("nill URL \(physicst.image)")
}
}else {
cell.imageView?.image = physicst.imageData!
}
return cell
}
}
**fell free to copy/paste it, there is no custom cell, so it should work **
tableView reload should be called on main thread. session.dataTaskWithRequest completion block is called on background thread performing UI Operations on background thread might lead to serious consequences. I believe the problem you are facing is just one of those consequences. Modify the code as follow.
func loadDataFromDBPedia() {
let session = NSURLSession.sharedSession()
var url = "http://dbpedia.org/sparql/"
let query = "http://dbpedia.org&query=select * {?o dbo:thumbnail ?p . ?o dbo:award dbr:Nobel_Prize_in_Physics} limit 10"
url = url + "?default-graph-uri=" + query.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())!
url = url + "&format=JSON&CXML_redir_for_subjs=121&CXML_redir_for_hrefs=&timeout=30000&debug=on"
let request = NSMutableURLRequest(URL: NSURL(string: url)!)
let task = session.dataTaskWithRequest(request, completionHandler: {(data, response ,error) in
if let error = error {
print ("\(error)")
}
if let response = response {
let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode
print("Status code = \(statusCode)")
}
if let data = data {
do {
let jsonResponse = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions())
let binding = jsonResponse["results"]!!["bindings"] as! NSArray
for oneBinding in binding {
let name = oneBinding["o"]!!["value"] as! NSString
var image = oneBinding["p"]!!["value"] as! String
image = image.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!
let physicst = Physicst(image: image, name: name)
if self.physicsts == nil {
self.physicsts = [Physicst]()
}
self.physicsts!.append(physicst)
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.physicstsTableView.reloadData()
}
}catch _ {
print ("not well json-formatted response")
}
}
})
task.resume()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("physicstCell")! as UITableViewCell
let row = indexPath.row
let physicst = self.physicsts![row]
cell.textLabel?.text = physicst.name as String
if (physicst.imageData == nil) {
let session = NSURLSession.sharedSession()
let url = NSURL(string: physicst.image as String)
if let url = url {
let request = NSMutableURLRequest(URL: url)
let task = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
if let data = data {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let imageData = UIImage(data: data)
cell.imageView?.image = imageData
physicst.imageData = imageData
self.physicstsTableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
})
}
})
task.resume()
}else {
print ("nill URL \(physicst.image)")
}
}else {
cell.imageView?.image = physicst.imageData!
}
return cell
}
TIP
Downloading the images manually for each cell and then loading it to tableViewCell and handling caching in order to improve the performance of scroll is like re inventing the wheel when you have tubless tires availabe :) Please consider using SDWebImage or AFNetworking I have personlly used SDWebImage and its caching feature works perfectly.

tableView cell is displaying the wrong image sometimes?

I have a tableView that displays an image in the cell. Most of the time the correct image will be displayed, however occasionally it will display the wrong image (usually if scrolling down the tableView very quickly). I download the images asynchronously and store them in a cache. Can't find what else could be causing the issue?? Below is the code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// try to reuse cell
let cell:CustomCell = tableView.dequeueReusableCellWithIdentifier("DealCell") as CustomCell
// get the venue image
let currentVenueImage = deals[indexPath.row].venueImageID
let unwrappedVenueImage = currentVenueImage
var venueImage = self.venueImageCache[unwrappedVenueImage]
let venueImageUrl = NSURL(string: "http://notrealsite.com/restaurants/\(unwrappedVenueImage)/photo")
// reset reused cell image to placeholder
cell.venueImage.image = UIImage(named: "placeholder venue")
// async image
if venueImage == nil {
let request: NSURLRequest = NSURLRequest(URL: venueImageUrl!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in
if error == nil {
venueImage = UIImage(data: data)
self.venueImageCache[unwrappedVenueImage] = venueImage
dispatch_async(dispatch_get_main_queue(), {
// fade image in
cell.venueImage.alpha = 0
cell.venueImage.image = venueImage
cell.venueImage.fadeIn()
})
}
else {
}
})
}
else{
cell.venueImage.image = venueImage
}
return cell
}
Swift 3
I write my own light implementation for image loader with using NSCache.
No cell image flickering!
ImageCacheLoader.swift
typealias ImageCacheLoaderCompletionHandler = ((UIImage) -> ())
class ImageCacheLoader {
var task: URLSessionDownloadTask!
var session: URLSession!
var cache: NSCache<NSString, UIImage>!
init() {
session = URLSession.shared
task = URLSessionDownloadTask()
self.cache = NSCache()
}
func obtainImageWithPath(imagePath: String, completionHandler: #escaping ImageCacheLoaderCompletionHandler) {
if let image = self.cache.object(forKey: imagePath as NSString) {
DispatchQueue.main.async {
completionHandler(image)
}
} else {
/* You need placeholder image in your assets,
if you want to display a placeholder to user */
let placeholder = #imageLiteral(resourceName: "placeholder")
DispatchQueue.main.async {
completionHandler(placeholder)
}
let url: URL! = URL(string: imagePath)
task = session.downloadTask(with: url, completionHandler: { (location, response, error) in
if let data = try? Data(contentsOf: url) {
let img: UIImage! = UIImage(data: data)
self.cache.setObject(img, forKey: imagePath as NSString)
DispatchQueue.main.async {
completionHandler(img)
}
}
})
task.resume()
}
}
}
Usage example
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Identifier")
cell.title = "Cool title"
imageLoader.obtainImageWithPath(imagePath: viewModel.image) { (image) in
// Before assigning the image, check whether the current cell is visible for ensuring that it's right cell
if let updateCell = tableView.cellForRow(at: indexPath) {
updateCell.imageView.image = image
}
}
return cell
}
I think the issue is with sendAsynchronousRequest. If you are scrolling faster than this is taking, when you reuse a cell, you can end up with the old completionHandler replacing the "wrong" cell (since it's now showing a different entry). You need to check in the completion handler that it's still the image you want to show.
So after some of the previous answers pointing me in the right direction, this is the code I added, which seems to have done the trick. The images all load and are displayed as they should now.
dispatch_async(dispatch_get_main_queue(), {
// check if the cell is still on screen, and only if it is, update the image.
let updateCell = tableView .cellForRowAtIndexPath(indexPath)
if updateCell != nil {
// fade image in
cell.venueImage.alpha = 0
cell.venueImage.image = venueImage
cell.venueImage.fadeIn()
}
})
This is the problem with dequeued re-usable cell. Inside the image download completion method, you should check whether this downloaded image is for correct index-path. You need to store a mapping data-structure that stores the index-path and a corresponding url. Once the download completes, you need to check whether this url belongs to current indexpath, otherwise load the cell for that downloaded-indexpath and set the image.
The following code changes worked me.
You can download the image in advance and save it in the application directory which is not accessible by the user. You get these images from the application directory in your tableview.
// Getting images in advance
var paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as! String
var dirPath = paths.stringByAppendingPathComponent("XYZ/")
var imagePath = paths.stringByAppendingPathComponent("XYZ/\(ImageName)" )
println(imagePath)
var checkImage = NSFileManager.defaultManager()
if checkImage.fileExistsAtPath(imagePath) {
println("Image already exists in application Local")
} else {
println("Getting Image from Remote")
checkImage.createDirectoryAtPath(dirPath, withIntermediateDirectories: true, attributes: nil, error: nil)
let request: NSURLRequest = NSURLRequest(URL: NSURL(string: urldata as! String)!)
let mainQueue = NSOperationQueue.mainQueue()
NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in
if let httpResponse = response as? NSHTTPURLResponse {
// println("Status Code for successful------------------------------------>\(httpResponse.statusCode)")
if (httpResponse.statusCode == 502) {
//Image not found in the URL, I am adding default image
self.logoimg.image = UIImage(named: "default.png")
println("Image not found in the URL")
} else if (httpResponse.statusCode == 404) {
//Image not found in the URL, I am adding default image
self.logoimg.image = UIImage(named: "default.png")
println("Image not found in the URL")
} else if (httpResponse.statusCode != 404) {
// Convert the downloaded data in to a UIImage object
let image = UIImage(data: data)
// Store the image in to our cache
UIImagePNGRepresentation(UIImage(data: data)).writeToFile(imagePath, atomically: true)
dispatch_async(dispatch_get_main_queue(), {
self.logoimg.contentMode = UIViewContentMode.ScaleAspectFit
self.logoimg.image = UIImage(data: data)
println("Image added successfully")
})
}
}
})
}
// Code in cellForRowAtIndexPath
var checkImage = NSFileManager.defaultManager()
if checkImage.fileExistsAtPath(imagePath) {
println("Getting Image from application directory")
let getImage = UIImage(contentsOfFile: imagePath)
imageView.backgroundColor = UIColor.whiteColor()
imageView.image = nil
imageView.image = getImage
imageView.frame = CGRectMake(xOffset, CGFloat(4), CGFloat(30), CGFloat(30))
cell.contentView.addSubview(imageView)
} else {
println("Default image")
imageView.backgroundColor = UIColor.whiteColor()
imageView.image = UIImage(named: "default.png")!
imageView.frame = CGRectMake(xOffset, CGFloat(4), CGFloat(30), CGFloat(30))
cell.contentView.addSubview(imageView)
}
Swift 5
So a simple solution to your problem would be by creating a custom class which subclasses UIImageView.
Add a property to store the url string.
Initially set the image to your placeholder to stop flickering.
While parsing and setting the image from response data compare class property url string with the url string passed through to the function and make sure they are equal.
Cache your image with the key as the url string and retrieve accordingly.
Note: Do not extend UIImageview as we plan to add property imageUrl.
reference: https://www.youtube.com/watch?v=XFvs6eraBXM
let imageCache = NSCache<NSString, UIImage>()
class CustomIV: UIImageView {
var imageUrl: String?
func loadImage(urlStr: String) {
imageUrl = urlStr
image = UIImage(named: "placeholder venue")
if let img = imageCache.object(forKey: NSString(string: imageUrl!)) {
image = img
return
}
guard let url = URL(string: urlStr) else {return}
imageUrl = urlStr
URLSession.shared.dataTask(with: url) { data, response, error in
if let err = error {
print(err)
} else {
DispatchQueue.main.async {
let tempImg = UIImage(data: data!)
if self.imageUrl == urlStr {
self.image = tempImg
}
imageCache.setObject(tempImg!, forKey: NSString(string: urlStr))
}
}
}.resume()
}
}
Just update you tableview cell's imageview to a CustomIV object.
And then update the image using:
cell.venueImage.loadImage(urlStr: "http://notrealsite.com/restaurants/\(unwrappedVenueImage)/photo")

Resources