iOS UIImageView not refreshing automatically - swift

I'm learning iOS programming by following this wonderful tutorial, except I'm targeting iOS 9 and make some small modifications.
In the tableView() function below, I can get thumbnail image downloaded and my handlers invoked, as evident from the console print out of the two logging lines. However, when the app is running (in simulator), I have to click on each table cell to get the image to show up. I tried to see if there is a refresh() or something like that in the UIImageView or the table cell, but found nothing.
How to make the image show up immediately as the data is received?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier)!
let album = self.albums[indexPath.row]
// ... setting up other cell's data, see the tutorial link
// Start by setting the cell's image to a static file
// Without this, we will end up without an image view!
cell.imageView?.image = UIImage(named: "Blank52")
let request: NSURLRequest = NSURLRequest(URL: thumbnailURL)
let urlSession = NSURLSession.sharedSession()
urlSession.dataTaskWithRequest(request, completionHandler: {(data, response, error) -> Void in
print("received thumbnail \(thumbnailURLString)") // reached
if error == nil {
// Convert the downloaded data in to a UIImage object
let image = UIImage(data: data!)
// Update the cell
dispatch_async(dispatch_get_main_queue(), {
print("...dispatched thumbnail image for \(thumbnailURLString)") // reached
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
}
})
}
}).resume()
return cell
}

This part doesn't make sense:
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
This seems to be creating an infinite loop. Every call to cellForRowAtIndexPath will cause another call to cellForRowAtIndexPath.
I think you just meant:
cell.imageView?.image = image
You already know the cell. You just created it before calling this block. You don't need to look it up again.

I figured out a work around. If I also update the text slight, the image shows up right the way without clicking on the table view cell.
cellToUpdate.imageView?.image = image
cellToUpdate.textLabel?.text = "\(album.title) " // a space is added to the end to make a difference and force the cell to be updated
Sounds like a bug in UITableViewCell (or the simulator? haven't tried on a real device) to me.

Why u just try without dispatch_async(dispatch_get_main_queue()) part.
dispatch_async(dispatch_get_main_queue(), {
print("...dispatched thumbnail image for \(thumbnailURLString)") // reached
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
}
})
to
cell.imageView?.image = image

You probably need to cast the cell when you get the cell you try to update.
dispatch_async(dispatch_get_main_queue(), {
print("...dispatched thumbnail image for \(thumbnailURLString)") // reached
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) as! UITableViewCell {
cellToUpdate.imageView?.image = image
}
})

Swift 5 Just put the following piece of code in the cellForRowAt delegate method after setting the image.
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.endUpdates()
}
// Full Method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "PhotoCell", for: indexPath) as? PhotoCell else { return UITableViewCell() }
let model = self.arrPhotos?[indexPath.row]
// Below method download image from url using asycn
SwiftHelper.downloadImageFrom(strUrl: model?.url ?? "") {[weak self] (img) in
guard let _ = self, let image = img else { return }
print("ImgDownloaded")
cell.imgView.image = image
// cell.layoutIfNeeded()
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.endUpdates()
}
}
return cell
}
To do animation on image you can remove performWithoutAnimation block.

Related

Replacing default images in a collection view with user selected photos- swift

So I have a collection view that populates three default images on each cell. The user can select images through the new PHPicker, what I want to accomplish is
to replace default images with selected photos, so the first selected image should replace the camera cell and so on ... (see images through links at the bottom)
to display the default images in case user deletes the selected images
currently when I send a new photo to the imgArray it gets displayed in a new cell before the camera cell, as I'm using insert method like so: imgArray.insert(image, at: 0).
My code:
var imgArray = [UIImage(systemName: "camera"), UIImage(systemName: "photo"), UIImage(systemName: "photo")]
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "1", for: indexPath) as! CustomCell
cell.imageView.image = imgArray[indexPath.row]
cell.imageView.tintColor = .gray
cell.imageView.layer.cornerRadius = 4
return cell
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
dismiss(animated: true, completion: nil)
for item in results {
item.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
if let image = image as? UIImage {
DispatchQueue.main.async {
self.imgArray.insert(image, at: 0)
self.collectionView.reloadData()
}
}
}
}
}
I have tried to remove the first item of the array, and then insert new photo like this:
self.imgArray.removeFirst(1)
self.imgArray.insert(image, at: 0)
self.collectionView.reloadData()
but this would work just one time as the code itself says, after that all replacement takes place just in the first cell.
So how can I get to other cells after first replacement? Any other approach that gives the same result will help me alot. Thanks in advance guys!
before selection
after selecting three images
One way to do this is to keep two image arrays, defaultImages of type [UIImage] and inputImages of type [UIImage?]. The defaultImages will hold your camera and photo images, and inputImages will hold images selected by the user. Initialize the images as:
defaultImages = [UIImage(systemName: "camera"), UIImage(systemName: "photo"), UIImage(systemName: "photo")]
inputImages: [UIImage?] = [nil, nil, nil]
To select the correct photo for index index use:
image = inputImages[index] ?? defaultImages[index]
To add a user-input image at index index use:
image: UIImage = ... // Get the image.
inputImages[index] = image
And to delete a user-input image at index index use:
inputImages[index] = nil

How to give Image from another thread?

I have a code for getting user pic:
if let photoURL = message[Constants.MessageFields.photoURL], let URL = URL(string: photoURL),
let data = try? Data(contentsOf: URL) {
cell.userPic.image = UIImage(data: data)
}
When I'm using it, tableView lagging at scrolling.
Please help me to put this code in another thread.
Here is a good sample provided by Apple, that you can adapt for your needs:
Prefetching collection view data
Basic idea is to create AsyncFetcher for your images and put image creation code to separate operation.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else {
fatalError("Expected `\(Cell.self)` type for reuseIdentifier \(Cell.reuseIdentifier). Check the configuration in Main.storyboard.")
}
let model = models[indexPath.row]
let id = model.id
cell.representedId = id
// Check if the `asyncFetcher` has already fetched data for the specified identifier.
if let fetchedData = asyncFetcher.fetchedData(for: id) {
// The data has already been fetched and cached; use it to configure the cell.
cell.configure(with: fetchedData)
} else {
// There is no data available; clear the cell until we've fetched data.
cell.configure(with: nil)
// Ask the `asyncFetcher` to fetch data for the specified identifier.
asyncFetcher.fetchAsync(id) { fetchedData in
DispatchQueue.main.async {
/*
The `asyncFetcher` has fetched data for the identifier. Before
updating the cell, check if it has been recycled by the
collection view to represent other data.
*/
guard cell.representedId == id else { return }
// Configure the cell with the fetched image.
cell.configure(with: fetchedData)
}
}
}
return cell
}
But in your case you should use Table View prefetching
I can confirm that this approach works and (when done right) results smooth scrolling and good UX

How to fetch the image from Firebase and save it to the Image in tableViewCell?

I am using a TableViewController and fetching image from firebase inside a Closure. I do get the name of the images in a local Database and saving it in an array at the time of fetching. But when I access the image array using indexPath.row everything gets collapsed(Instead of 2 images I need, all the images are getting replaced and it keeps on getting increased when I scroll the tableView). Please do guide me how to fetch the image inside the cell. Thanks in advance
var imageList = [UIImage]()
func getData()
{
Data = NSMutableArray()
Data = ModelManager.getInstance().getAllData(planID)
let count = Data.count
for i in 0...count-1{
let demoPlan = (Data.object(at: i) as! workoutPlan)
ImageDemo.append(demoPlan.Image)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell2:workoutplanCell = tableView.dequeueReusableCell(withIdentifier: "workoutplanCell") as! workoutplanCell
cell2.planExerciseNameLabel.text = plan.Name
if(plan.Image.contains("plank")){
cell2.timerButton.isHidden = false
}
else{
cell2.timerButton.isHidden = true
}
var new_imgList = [String]()
new_imgList.append(ImageDemo[indexPath.row])
new_imgList.append(ImageDemo[indexPath.row] + "1")
let storage = Storage.storage().reference()
for x in new_imgList
{
let storageRef = storage.child("images/\(new_imgList[x]).jpg")
storageRef.getData(maxSize: 1 * 1024 * 1024) { (data, error) in
if let error = error{
print(error.localizedDescription)
return
}
else{
self.imageList.append(UIImage(data: data!)!)
}
if x == new_imgList{
cell2.planExerciseImage.animationImages = self.imageList
cell2.planExerciseImage.animationDuration = 2
cell2.planExerciseImage.startAnimating()
cell2.planDescription.delegate = self
cell2.planDescription.text = plan.Description
cell2.planDescription.sizeToFit()
cell2.planDescription.textColor = UIColor.black
self.imageList.removeAll()
new_imgList.removeAll()
}
}
}
return cell2
}
You probably have that problem for two reasons:
One is the imageList scope. You have defined it somewhere in your ViewController and everytime a cell is set it keeps adding images.
And the second reason that is somewhat connected to the first one, is the if x == 1 i cant see that statement being called all the time, consequently your imageList will not be cleared for every cell.
Remove your new_imgList from wherever you have declared it and instead declare it here:
for x in new_imgList{
// DECLARE new_imgList HERE
.....
}
and delete the self.imageList.removeAll() line.

CollectionView Cell and Progress Bar - Progress Bar showing in wrong Cell after scrolling

Ive been searching for a answer to this one for days now and cant seem to figure it out. I have a Collection View with custom cell. When you double tap a cell in the Collection View it will either download a file or delete it if its been downloaded before.
During the download a progress bar displays the progress of the download then displays a small icon in the top left corner. When deleting it removes the icon.
If you download from one cell and delete from another while first download is in progress it works fine but only if both cells were visible within the collection view.
if i download from one cell, then scroll offscreen and delete from a cell that is not in same screen as the cell that is being download from, it removes the corner image as usual then displays the progress bar of the cell that is being download from.
I don't know if this is an error with how i am reusing cells??? It doesn't seem to have anything to do with how i am updating the cell or collection view which works in all cases except after scrolling.
Below is 2 functions that download or delete file:
func downloadDataToDevice(cell: JourneyCollectionViewCell, selectedIndexPath: IndexPath){
let downloadedAudio = PFObject(className: "downloadedAudio")
// save all files with unique name / object id
let selectedObjectId = self.partArray[selectedIndexPath.item].id
let selectedPartName = self.partArray[selectedIndexPath.item].name
let query = PFQuery(className: "Part")
query.whereKey("objectId", equalTo: selectedObjectId)
query.getFirstObjectInBackground { (object, error) in
if error != nil || object == nil {
print("No object for the index selected.")
} else {
//print("there is an object, getting the file.")
downloadedAudio.add(object?.object(forKey: "partAudio") as! PFFile, forKey: selectedPartName)
let downloadedFile = object?.object(forKey: "partAudio") as! PFFile
// get the data first so we can track progress
downloadedFile.getDataInBackground({ (success, error) in
if (success != nil) {
// pin the audio if there is data
downloadedAudio.pinInBackground(block: { (success, error) in
if success {
// reload the cell
self.reloadCell(selectedIndexPath: selectedIndexPath, hideProgress: true, hideImage: false, cell: cell)
self.inProgress -= 1
cell.isUserInteractionEnabled = true
}
})
}
// track the progress of the data
}, progressBlock: { (percent) in
self.activityIndicatorView.stopAnimating()
cell.progessBar.isHidden = false
//cell.progessBar.transform = cell.progessBar.transform.scaledBy(x: 1, y: 1.1)
cell.contentView.bringSubview(toFront: cell.progessBar)
cell.progessBar.setProgress(Float(percent) / Float(100), animated: true)
cell.isUserInteractionEnabled = false
})
}
}
}
func removeDataFromDevice(cell: JourneyCollectionViewCell, selectedIndexPath: IndexPath, object: PFObject) {
let selectedPartName = self.partArray[selectedIndexPath.item].name
// unpin the object from the LocalDataStore
PFObject.unpinAll(inBackground: [object], block: { (success, error) in
if success {
// reduce inProgress
self.inProgress -= 1
self.reloadCell(selectedIndexPath: selectedIndexPath, hideProgress: true, hideImage: true, cell: cell)
}
})
}
and this is how I'm reloading the cell
func reloadCell(selectedIndexPath: IndexPath, hideProgress: Bool, hideImage: Bool, cell: JourneyCollectionViewCell) {
cell.progessBar.isHidden = hideProgress
cell.imageDownloaded.isHidden = hideImage
self.collectionView.reloadItems(at: [selectedIndexPath])
}
----------- EDIT -------------
This is my cellForItem at function. Presently i am using a query to look on local drive and see if the file exists and then adding the corner image if it is. This is the first time i have used a query in this place, usually it is a query at login to populate an array but that is for a more static collection of data than what i am trying to achieve here by letting the user download and delete files.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: JourneyCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! JourneyCollectionViewCell
cell.imageCell.file = self.partArray[indexPath.item].image
cell.imageCell.loadInBackground()
cell.imageCell.layer.masksToBounds = true
// not sure if its good to run a query here as its constantly updated.
// query if file is on LDS and add image to indicate
let cellPartName = self.partArray[indexPath.item].name
let checkQuery = PFQuery(className: "downloadedAudio")
checkQuery.whereKeyExists(cellPartName)
checkQuery.fromLocalDatastore()
checkQuery.getFirstObjectInBackground(block: { (object, error) in
if error != nil || object == nil {
//print("The file does not exist locally on the device, remove the image.")
cell.imageDownloaded.isHidden = true
cell.imageDownloaded.image = UIImage(named: "")
cell.progessBar.isHidden = true
} else {
//print("the file already exists on the device, add the image.")
cell.contentView.bringSubview(toFront: cell.imageDownloaded)
cell.imageDownloaded.isHidden = false
cell.imageDownloaded.image = UIImage(named: "download-1")
}
})
return cell
}
This is a normal feature of "reuse" cells, for efficient memory management purposes. What you need to do is reset the cell values in below function:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
}
By reset, I mean set the cells to their default state, prior to you making any updates such as adding the left corner icon or the status bar.
You need to make sure the arrays that you are feeding the collectionview data from is maintained properly. For example, if you have an array A =[1,2,3] and you delete A[1], then array A needs to be [1,3].
So i tried placing the progress view programatically, i tried prepareForReuse in the custom cell class, neither resolved this issue directly, though i will keep using prepareForReuse as i think its a cleaner way to manage the cell than i had been.
What seems to have worked was relocating the cell within the progressBlock
if let downloadingCell = self.collectionView.cellForItem(at: selectedIndexPath) as? JourneyCollectionViewCell { downloadingCell.progessBar.isHidden = false
downloadingCell.contentView.bringSubview(toFront: downloadingCell.progessBar)
downloadingCell.progessBar.setProgress(Float(percent) / Float(100), animated: true)
downloadingCell.setNeedsDisplay()
downloadingCell.isUserInteractionEnabled = false
}

Swift 2: UICollectionView - is it possible to return cell asynchronously?

I have a UICollectionView in a UIView container, and I use it for displaying images (as an array of PFFile) attached to a PFObject stored in Parse. I know I could fetch the the PFFile/image synchronously in my current implementation (see below working code for synchronous loading), but I would really like to make this an asynchronous process.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("attachedImageCell", forIndexPath: indexPath) as! AttachedImageCollectionViewCell
if let uploadImageFile = attachedImageFiles[indexPath.row] as? PFFile {
do {
var imageData: NSData
try imageData = uploadImageFile.getData()
cell.imageView.image = UIImage(data: imageData)
} catch let err as NSError {
print("error getting image file data: \(err)")
}
/*
//asynchronously getting image data - don't know how to return cell here
uploadImageFile.getDataInBackgroundWithBlock({ (imageData, error) -> Void in
if error != nil {
//TODO: show error in download
}
if imageData != nil {
cell.imageView.image = UIImage(data: imageData!)
}
})
*/
}
return cell
}
One way I have been contemplating is to have the AttachedImageCollectionViewCell object observe its UIImageView, once a UIImage file (a pointer) is assigned to it, it will go fetching the the PFFile from Parse and parse it to NSData, which is used for the UIImage.
As I am still on the learning curve of mastering Swift, I am not sure how feasible this approach might be. So I am all ears for any ideas here, thanks!
So in your code where you create the cell, you ensure, you set the image to cell asynchronously using the library-
https://github.com/natelyman/SwiftImageLoader/blob/master/ImageLoader.swift
//Create cell in usual way
//Code here to create the cell
//Load the image needed by the cell asynchronously
ImageLoader.sharedLoader.imageForUrl(urlString, completionHandler:{(image: UIImage?, url: String) in
cell.image = image
})
HTH