Swift update tableview async with S3 download function - ios

I have an async problem - I have a function that loads images from S3, stores them into an array of UIImages (here called Images)
I also have a tableview that loads its cells from firebase fetched data, my question is, how to update the cell image once the async finishes loading ?
I'm also afraid that the queue of images won't match exactly the indexPath.row since some images might load faster than other images.
func download(key:String, myindex:NSIndexPath, myrow:Int) -> NSString {
let path:NSString = NSTemporaryDirectory().stringByAppendingString("image.jpg")
let url:NSURL = NSURL(fileURLWithPath: path as String)
// let downloadingFilePath = downloadingFileURL.path!
let downloadRequest = AWSS3TransferManagerDownloadRequest()
downloadRequest.bucket = "witnesstest/" + rootref.authData.uid
downloadRequest.key = key
downloadRequest.downloadingFileURL = url
switch (downloadRequest.state) {
case .NotStarted, .Paused:
let transferManager = AWSS3TransferManager.defaultS3TransferManager()
transferManager.download(downloadRequest).continueWithBlock({ (task) -> AnyObject! in
if let error = task.error {
if error.domain == AWSS3TransferManagerErrorDomain as String
&& AWSS3TransferManagerErrorType(rawValue: error.code) == AWSS3TransferManagerErrorType.Paused {
print("Download paused.")
} else {
print("download failed: [\(error)]")
}
} else if let exception = task.exception {
print("download failed: [\(exception)]")
} else {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let tempimage:UIImage = UIImage(contentsOfFile: path as String)!
print("dl ok")
self.Images.append(tempimage)
})
}
return nil
})
break
default:
break
}
return path
}
and the cell :
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: MainWitnessTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("RIcell") as! MainWitnessTableViewCell
// populate the cell
let postitem = posts[indexPath.row]
cell.postOwner.text = postitem.author
cell.postContent.text = postitem.content
cell.postDate.text = postitem.createon
let myindex = indexPath
let myrow = indexPath.row
// cell.cellImage.image = Images[indexPath.row] // returns array index out of range
// download(postitem.imagekey, myindex: myindex, myrow: myrow)
return cell
}

You can just set the imageView's image after you download the image and at the same time set postitem's image property (assuming you add one). It's hard for me to understand everything your download method is doing, but I think the gist of what you want is something like:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: MainWitnessTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("RIcell") as! MainWitnessTableViewCell
// populate the cell
let postitem = posts[indexPath.row]
cell.postOwner.text = postitem.author
cell.postContent.text = postitem.content
cell.postDate.text = postitem.createon
//postitem should also have a imageURL property or you should have some way of getting the image url.
if post item.image == nil{
dispatch_queue_t imageFetchQ = dispatch_queue_create("image fetcher", NULL)
dispatch_async(imageFetchQ, ^{
let imageData = NSData(contentsOfURL: postitem.imageURL)
let image = UIImage(data: imageData)
postitem.image = image
dispatch_async(dispatch_get_main_queue(), ^{
//the UITableViewCell may have been dequeued and reused so check if the cell for the indexPath != nil
if tableView.cellForRowAtIndexPath(indexPath) != nil{
cell.imageView.image = image
}
})
})
return cell
}

I would recommend you create a cache of images and the indices they're supposed to be associated with, and then instead of loading all the images in your function, you would create the table view, and then for that specific cell ask for the image from your server or from the cache if it's already there, rather then trying to download them all async at one time

Related

error while loading images asynchronously from firebase

I have been trying to load images asynchronously for quiet a while now.
When i make a new class for this and built and run the app an error appears in the runtime log
// log
Failed to set (borderWidth) user defined inspected property on ...
could not set nil as the value for the key borderWidth.
could not set nil as the value for the key cornerRadius.
It is fine with extension instead of class, the images load async but, this way I will not be able to make sure the correct image is loaded in the image view.
// cell for item at
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "subscribeCell", for: indexPath) as! subscribeViewCell
cell.userID = self.users[indexPath.row].userId
cell.profileName.text = self.users[indexPath.row].fullName
cell.profileImage.setupWithImageUrl(imageUrl: self.users[indexPath.row].ImagePath)
checkFollowers(indexPath : indexPath)
return cell
}
the class i have been working with in the same view controller :
Class to download image:
private var imageCache = [String:UIImage]()
class CustomImageView : UIImageView {
var lastImageUrl : String?
func setupWithImageUrl (imageUrl: String) {
self.contentMode = .scaleAspectFill
self.clipsToBounds = true
self.layer.cornerRadius = 14
self.layer.borderWidth = 0
lastImageUrl = imageUrl
if let cachedImage = imageCache[imageUrl] {
self.image = cachedImage
return
}
guard let url = URL(string: imageUrl) else {
return
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print(error)
return
}
guard let data = data else {
return
}
guard let image = UIImage(data : data) else {
return
}
imageCache[url.absoluteString] = image
}
if (url.absoluteString != self.lastImageUrl) {
return
}
DispatchQueue.main.async {
self.image = self.image
}
}
}
Why "this way" cannot make sure correct image is loaded in the image view. I read your code I think your code is fine for collectionViewController cellForItemAt

Set a default image in TableViewCell

I have tried several different approaches and nothing has yet to work. I am pulling in album artwork for a recently played tableview for my radio station app. I get blank images when there is no album artwork to pull into the cell. I just want to have my station logo "WhiteLogo.png" as a placeholder whenever there is no album artwork pulled into the tableview cell. Any help in the right direction is much appreciated. Thanks
import UIKit
//----------
//MARK: JSON
//----------
//The Initial Response From The JSON
struct Response: Codable {
var playHistory: Album
}
//The Album Received Which Is An Array Of Song Data
struct Album: Codable {
var song: [SongData]
}
//The SongData From The PlayHistory Album
struct SongData: Codable{
var album: String
var artist: String
var cover: String
var duration: String
var programStartTS: String
var title: String
}
class TableViewController: UITableViewController {
//1. Create An Array To Store The SongData
var songs = [SongData]()
var currentStation: RadioStation!
var downloadTask: URLSessionDownloadTask?
override func viewDidLoad() { super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
//2. Load The JSON From The Main Bundle
guard let urlText = URL (string: "http://streamdb3web.securenetsystems.net/player_status_update/JACKSON1_history.txt")
else { return }
do{
//a. Get The Data From The From The File
let data = try Data(contentsOf: urlText)
//b. Decode The Data To Our Structs
let albumData = try JSONDecoder().decode(Response.self, from: data)
//c. Append The Songs Array With The PlayHistory
albumData.playHistory.song.forEach { songs.append($0) }
//d. Test Some Data
print("""
**The First Album Details**
Album = \(songs[0].album)
Artist = \(songs[0].artist)
Cover = \(songs[0].cover)
Duration = \(songs[0].duration)
Start = \(songs[0].programStartTS)
Title = \(songs[0].title)
""")
//3. Load The Data
DispatchQueue.main.async {
self.tableView.reloadData()
}
}catch{
print(error)
}
}
//-----------------
//MARK: UITableView
//-----------------
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return songs.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//1. Create A Cell
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
//2. Set It's Text
cell.songTitle.text = songs[indexPath.row].title
cell.artistLabel.text = songs[indexPath.row].artist
//3. Get The Image
if let imageURL = URL(string: songs[indexPath.row].cover){
let request = URLSession.shared.dataTask(with: imageURL) { (imageData, response, error) in
if let error = error{
print(error)
}else{
guard let image = imageData else { return }
DispatchQueue.main.async {
cell.songCover.image = UIImage(data: image)
cell.setNeedsLayout()
cell.layoutIfNeeded()
}
}
}
request.resume()
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("""
**Album \(indexPath.row) Selected**
Album = \(songs[indexPath.row].album)
Artist = \(songs[indexPath.row].artist)
Cover = \(songs[indexPath.row].cover)
Duration = \(songs[indexPath.row].duration)
Start = \(songs[indexPath.row].programStartTS)
Title = \(songs[indexPath.row].title)
""")
}
}
Just the right case handling is required.
I would set the placeholder image first and then proceed to download an image from a URL.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//...
/*
Start with placeholder image so it shows until the image download completes.
And if the next `if let imageURL` condition fails, the placeholder image will stay
*/
cell.songCover.image = UIImage(named: "WhiteLogo")
//Continue with your logic, no change really but could be shortened to:
if let imageURL = URL(string: songs[indexPath.row].cover) {
let request = URLSession.shared.dataTask(with: imageURL) { (imageData, response, error) in
guard let imageData = imageData else { return }
DispatchQueue.main.async {
cell.songCover.image = UIImage(data: imageData)
}
}
request.resume()
}
//...
}
However, since the image download logic is async, it will misbehave if the cell is reused before the download completes.
i.e. Image download for the first song starts but you scroll fast enough to reuse the first cell for, lets say, the third song.
Now, when the download completes, the first image could show on the third cell.
If you face this issue then let me know and I shall update my answer.
Set "WhiteLogo.png" on above your code which download image for album or set logo image if album image data is nil like guard let image = imageData else { var image : UIImage = UIImage(named:"WhiteLogo.png")!
cell.songCover.image = UIImageView(image: image) }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//1. Create A Cell
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
//2. Set It's Text
cell.songTitle.text = songs[indexPath.row].title
cell.artistLabel.text = songs[indexPath.row].artist
//set image
var image : UIImage = UIImage(named:"WhiteLogo.png")!
cell.songCover.image = UIImageView(image: image)
//3. Get The Image
if let imageURL = URL(string: songs[indexPath.row].cover){
let request = URLSession.shared.dataTask(with: imageURL) { (imageData, response, error) in
if let error = error{
print(error)
}else{
guard let image = imageData else { return }
DispatchQueue.main.async {
cell.songCover.image = UIImage(data: image)
cell.setNeedsLayout()
cell.layoutIfNeeded()
}
}
}
request.resume()
}
return cell
}
guard let image = imageData else { cell.songCover.image = UIImage(named : "your_image_name"); return }
Please use the Kingfisher library it will download image from url and set placeholder image.Library URL:- https://github.com/onevcat/Kingfisher

Image in table view [duplicate]

This question already has an answer here:
Asynchronously load image in uitableviewcell
(1 answer)
Closed 5 years ago.
I have a problem with the image in table view cell
The images are downloaded form the Google Firebase and in every cell there is one of that
But when I scroll up or down the images change automatically the index
Here is my code, someone can help me? thanks a lot!
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if postsArray[indexPath.row].imageNoImage == true{
let cell = tableView.dequeueReusableCell(withIdentifier: "imageLook", for: indexPath) as! imageLookTableViewCell
cell.authorLabel.text = self.postsArray[indexPath.row].author
cell.likesCountLabel.text = "\(self.postsArray[indexPath.row].likes!)"
cell.postID = postsArray[indexPath.row].postId
cell.textViewPost.text = self.postsArray[indexPath.row].textPost
let url = URL(string: postsArray[indexPath.row].pathToImage as! String)
if url != nil {
DispatchQueue.global().async {
let data = try? Data(contentsOf: url!)
DispatchQueue.main.async {
if data != nil {
cell.imagePost.image = UIImage(data:data!)
}else{
}
}
}
}
for person in self.postsArray[indexPath.row].peopleWhoLike {
if person == FIRAuth.auth()!.currentUser!.uid {
cell.likesBtn.isHidden = false
break
}
}
return cell
}
Your question is not very descriptive/well written, but I think your problem is that you are not caching your images.
Try this:
let imageCache = NSCache()
let cell = tableView.dequeueReusableCell(withIdentifier: "imageLook", for: indexPath) as! imageLookTableViewCell
cell.authorLabel.text = self.postsArray[indexPath.row].author
cell.likesCountLabel.text = "\(self.postsArray[indexPath.row].likes!)"
cell.postID = postsArray[indexPath.row].postId
cell.textViewPost.text = self.postsArray[indexPath.row].textPost
let url = URL(string: postsArray[indexPath.row].pathToImage as! String)
if url != nil {
if let cachedImage = imageCache.objectForKey(url) as? UIImage {
cell.imagePost.image = cachedImage
return
}
DispatchQueue.global().async {
let data = try? Data(contentsOf: url!)
DispatchQueue.main.async {
if data != nil {
if let myImageName = UIImage(data:data!){
imageCache.setObject(myImageName, forKey: url)
}
cell.imagePost.image = UIImage(data:data!)
}else{
}
}
}
}
for person in self.postsArray[indexPath.row].peopleWhoLike {
if person == FIRAuth.auth()!.currentUser!.uid {
cell.likesBtn.isHidden = false
break
}
}
return cell
}

Table view lag while displaying images from network

I am creating a small app for displaying images in TableView from Flickr. The app works fine but i am facing a issue while scrolling table view, my tableview lags a lot. I think the issue might be with GCD or Threads, i am new to networking and GCD, this is my code getting images from Flickr API.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = flickrTableView.dequeueReusableCellWithIdentifier("cell") as! FlickerTableCell
let flickr = flickrArray[indexPath.row]
dispatch_async(dispatch_get_main_queue(), {
cell.imageViewFlickr.image = flickr.getFlickrImage()
})
cell.labelFlickrTitle.text = flickr.title
return cell
}
// function to get images from flickr
func getFlickrImage()->UIImage{
let imageURL = NSURL(string: "https://farm\(farm).staticflickr.com/\(server)/\(photoId)_\(secret)_m.jpg")!
var flickrImage = UIImage()
if let imageData = NSData(contentsOfURL: imageURL) {
flickrImage = UIImage(data: imageData)!
} else {
print("Image does not exist at \(imageURL)")
}
return flickrImage
}
Your method to fetch image from network that is getFlickrImage() is sync method so table cell waits for its response and as the network call is in main thread it freezes or lags the UI. you can wrap your network call in async method and use completion handler to update the UI in main thread such as:
func getFlickrImage(completion:UIImage ->()){
let imageURL = NSURL(string: "https://farm\(farm).staticflickr.com/\(server)/\(photoId)_\(secret)_m.jpg")!
var flickrImage = UIImage()
let download = dispatch_queue_create("download", nil)
dispatch_async(download) {
if let imageData = NSData(contentsOfURL: imageURL) {
dispatch_async(dispatch_get_main_queue(), {
flickrImage = UIImage(data: imageData)!
completion(flickrImage)
})
} else {
print("Image does not exist at \(imageURL)")
completion(flickrImage)
}
}
}
Your cellForRowAtIndexPath will be like:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = flickrTableView.dequeueReusableCellWithIdentifier("cell") as! FlickerTableCell
let flickr = flickrArray[indexPath.row]
flickr.getFlickrImage { (photo) in
dispatch_async(dispatch_get_main_queue(), {
cell.imageViewFlickr.image = photo
})
}
cell.labelFlickrTitle.text = flickr.title
return cell
}

Displaying image from database in TableView bugs

I have a TableView and I am filling it with data retrieved from database. Everything works fine except the images. Because of the cell reuse behaviour, and I am fetching image in cellForRowAtIndexPath. I chose to fetch images in cellForRowAtIndexPath because in the details retrieving function (which is triggered in viewDidLoad), I need to do another request, which is causing other problems (reloading tableview before storing image url)
The problem is that when I scroll fast, the resuable cells bugs while displaying user images
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
}
var theUser =
func fetchData() {
//.. after data is retrieved
var innerDict = [String:String]()
if let user = details.value![key] {
if let name = user["name"] {
// works
innerDict["name"] = name
}
if let image = user["imageName"] {
// gets the image name but at this point I need to;
// a) retrieve the url here (with another call), which will eventually fail
// to catch up with `innerDict` so `innerDict` won't contain `image` variable.
// ie;
myRef.downloadURLWithCompletion { (URL, error) -> Void in }
// b) Store the `image` name in innerDict and download image from url
// in `cellForRowAtIndexPath`. I chose this method:
innerDict["image"] = image
}
user[id] = innerDict
tableView.reloadData()
}
Now the tableView as usual.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = ...
// more stuff
if let imageName = user["image"] {
let storage = FIRStorage.storage()
let storageRef = storage.referenceForURL("gs://bucket.com").child(user[id]).child(imageName)
storageRef.downloadURLWithCompletion { (URL, error) -> Void in
if (error != nil) {
// handle
} else {
// I thought using Haneke would help to cache the image
cell.image.hnk_setImage(URL!)
}
}
}
This is the closest one I could reach. However images bug on displaying when I scroll fast.
Edit:
I also tried using this approach but it's downloading the same image multiple times with this method, so it takes time for the same images to displayed.
islandRef.dataWithMaxSize(1 * 1024 * 1024) { (data, error) -> Void in
if (error != nil) {
// Uh-oh, an error occurred!
} else {
let image: UIImage! = UIImage(data: data!)
cell.userImage.hnk_setImage(image, key: "\(userID)")
}
}
However, with top approach the speed was very fast. The only problem of the above code was the glitch when I scroll fast.
Edit 2
var images = [UIImage]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ItemCell", forIndexPath: indexPath) as! ItemDetailTableViewCell
let item = items[indexPath.section][indexPath.row]
if let uid = item["owner"] as? String {
if let user = users[uid] {
if let imageName = user["image"] {
if let img: UIImage = images[indexPath.row] { // crash here "fatal error: Index out of range"
cell.userImage.image = img
}
} else {
let storage = FIRStorage.storage()
let storageRef = storage.referenceForURL("gs://bucket").child(uid).child(imageName)
storageRef.downloadURLWithCompletion { (URL, error) -> Void in
if (error != nil) {
} else {
dispatch_async(dispatch_get_main_queue(), {
cell.userImage.hnk_setImageFromURL(URL!)
self.images[indexPath.row] = cell.image.image!
})
}
}
}
}
}
I think you should save images from url and show images when the cell is going to reuse, hopefully this will fix your glitch
var myImages = [String: UIImage]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ItemCell", forIndexPath: indexPath) as! ItemDetailTableViewCell
let item = items[indexPath.section][indexPath.row]
if let img: UIImage = myImages["\(indexPath.section)\(indexPath.row)"] {
cell.image.image = img
} else {
if let uid = item["owner"] as? String {
if let imageName = user["image"] {
let storage = FIRStorage.storage()
let storageRef = storage.referenceForURL("gs://bucket.com").child(user[id]).child(imageName)
storageRef.downloadURLWithCompletion { (URL, error) -> Void in
if (error != nil) {
cell.image = UIImage(named: "placeholder") // put default Image when failed to download Image
} else {
dispatch_async(dispatch_get_main_queue(), {
cell.image.hnk_setImage(URL!)
// Store the image in to our cache
self.myImages["\(indexPath.section)\(indexPath.row)"]= cell.image.image
})
}
}
}
}
}

Resources