Animate cell.imageview on async load - ios

Im trying to do a table where the imageView on the cell changes alpha from 0 to 1 when the image is done loading (async).
What ever I do it seem that the image is just shown at one and not fading in. I'm sure it's some kind of race condition but I am new to animations in iOS and have no idea how to solve this. Any input would be great.
Here is my code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
//Configure the cell...
let episode = episodes[indexPath.row]
cell.textLabel?.text = episode.title
cell.detailTextLabel?.text = episode.content
let logoUrl = URL(string: episode.logoUrl!)
if (episode.logoImage == nil){
episode.logoImage = UIImage()
DispatchQueue.global().async {
let data = try? Data(contentsOf: logoUrl!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
DispatchQueue.main.async {
episode.logoImage = UIImage(data: data!)
cell.imageView?.image = episode.logoImage
self.episodesTable.reloadData()
cell.imageView?.alpha = 0
UIView.animate(withDuration: 1, animations: {
cell.imageView?.alpha = 1
})
}
}
} else{
cell.imageView?.image = episode.logoImage
}
return cell
}

You need to set alpha to 0 first before animating to 1.
cell.imageView?.alpha = 0
UIView.animate(withDuration: 1, animations: {
cell.imageView?.alpha = 1
})
Also, you dont need to reload table. Remove self.episodesTable.reloadData().
You are spanning a background thread and loading the image from url inside that thread. What if, in between user has scrolled the cell. You would be left with a wrong image on a wrong cell(because of cell reuse, that is).
My advice is to use SDWebImageCache, and use its completion block to animate the alpha.
// Changing animation duration to 0.2 seconds from 1 second
if(cacheType == SDImageCacheTypeNone) {
cell.imageView?.alpha = 0
[UIView animateWithDuration:0.2 animations:^{
cell.imageView?.alpha = 1;
}];
}

reloadData() call is causing reloading of all the cells including the one you are trying to animate. My advice is to mark your cell with it's index path. After async call check if it is still presenting the right data and animate it without reloading the whole table view.
// ...
cell.tag = indexPath.item
DispatchQueue.global().async {
// async load
DispatchQueue.main.async {
guard cell.tag == indexPath.item else { return }
cell.imageView?.alpha = 0.0
cell.imageView?.image = image
// animate
}
}
// ...

Related

How to get the indexPath of the current cell

I have currently trying to create a way to delete an item in a collection view at the indexPath of 1 back. So far i have used some help to create a function with scrollview did scroll to create a way to count which image the user is on by the current image method. I now need a way to count which cell the user is on. Here is my code>
var currentImage = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let x = floor(myCollectionView.contentOffset.x / view.frame.width)
if Int(x) != currentImage {
currentImage = Int(x)
//print(currentImage)
}
if currentImage > 0 {
for collectionCell in myCollectionView.visibleCells as [UICollectionViewCell] {
let indexPath = myCollectionView.indexPath(for: collectionCell as UICollectionViewCell)!
let indexPathOfLastItem = (indexPath?.item)! - 1
let indexPathOfItemToDelete = IndexPath(item: (indexPathOfLastItem), section: 0)
imageArray.remove(at: 0)
myCollectionView.deleteItems(at: [indexPathOfItemToDelete])
currentImage = 1
Based more on the comments than your actual question, what you seem to want is to get the first visible cell's index path so you can use that path to delete the cell.
let visibleCells = myCollectionView.visibleCells
if let firstCell = visibleCells.first() {
if let indexPath = myCollectionView.indexPath(for: collectionCell as UICollectionViewCell) {
// use indexPath to delete the cell
}
}
None of this should be used or done in the scrollViewDidScroll delegate method.
[Updated for Swift 5.2] Here's a slightly more succinct way than #rmaddy's answer (but that one works just fine):
guard let indexPath = collectionView.indexPathsForVisibleItems.first else {
return
}
// Do whatever with the index path here.

Custom pop animation to a UICollectionViewController doesn't work

I have a collection view called ProfileCollectionViewController for collection of images.
When clicked on an image it presents a HorizontalProfileViewController which displays images in full screen.
When back button is pressed on HorizontalProfileViewController I want the full screen image to animate back to a thumbnail in ProfileViewController. I pass the selected index path from ProfileViewController as initialIndexPath to HorizontalProfileViewController so that the position of thumbnail is known. Below is my transition animation code
import UIKit
class SEHorizontalFeedToProfile: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.2
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
if let profileVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as? SEProfileGridViewController, horizontalFeedVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as? SEProfileHorizontalViewController, containerView = transitionContext.containerView() {
let duration = transitionDuration(transitionContext)
profileVC.collectionView?.reloadData()
if let indexPath = horizontalFeedVC.initialIndexPath {
let cell = profileVC.collectionView?.cellForItemAtIndexPath(indexPath)
print(indexPath)
let imageSnapshot = horizontalFeedVC.view.snapshotViewAfterScreenUpdates(false)
let snapshotFrame = containerView.convertRect(horizontalFeedVC.view.frame, fromView: horizontalFeedVC.view)
imageSnapshot.frame = snapshotFrame
horizontalFeedVC.view.hidden = true
profileVC.view.frame = transitionContext.finalFrameForViewController(profileVC)
containerView.insertSubview(profileVC.view, belowSubview: horizontalFeedVC.view)
containerView.addSubview(imageSnapshot)
UIView.animateWithDuration(duration, animations: {
var cellFrame = CGRectMake(0, 0, 0, 0)
if let theFrame = cell?.frame {
cellFrame = theFrame
}
let frame = containerView.convertRect(cellFrame, fromView: profileVC.collectionView)
imageSnapshot.frame = frame
}, completion: { (succeed) in
if succeed {
horizontalFeedVC.view.hidden = false
// cell.contentView.hidden = false
imageSnapshot.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
})
}
}
}
}
I put breakpoints and found out that in the code
let cell = profileVC.collectionView?.cellForItemAtIndexPath(indexPath)
the cell is nil. I don't understand why it would be nil. Please help me. I thank you in advance.
The profileVC is a subclass of UICollectionViewController
PS: Please check out the following project that does exactly the same thing without any issues. I tried to mimic it but it doesn't work on mine.
https://github.com/PeteC/InteractiveViewControllerTransitions
As was pointed out in the comments on the question, the cell is set to nil because the cell is not visible. Since your HorizontalProfileViewController allows scrolling to other pictures it is possible to scroll to an image that has not yet had a UICollectionViewCell created for it in your ProfileCollectionViewController.
You can remedy this by scrolling to where the current image should be on the UICollectionView. You say that initialIndexPath is initially the indexPath of the image that was first selected. Assuming you update that as the user scrolls to different images so that it still accurately represents the on screen image's indexPath, we can use that to force the UICollectionView to scroll to the current cell if necessary.
With a few tweaks to your code this should give you the desired effect:
import UIKit
class SEHorizontalFeedToProfile: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.2
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
if let profileVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as? SEProfileGridViewController, horizontalFeedVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as? SEProfileHorizontalViewController, containerView = transitionContext.containerView() {
let duration = transitionDuration(transitionContext)
//profileVC.collectionView?.reloadData() //Remove this
if let indexPath = horizontalFeedVC.initialIndexPath {
var cell = profileVC.collectionView?.cellForItemAtIndexPath(indexPath) //make cell a var so it can be reassigned if necessary
print(indexPath)
if cell == nil{ //This if block is added. Again it is crucial that initialIndexPath was updated as the user scrolled.
profileVC.collectionView?.scrollToItemAtIndexPath(horizontalFeedVC.initialIndexPath, atScrollPosition: UICollectionViewScrollPosition.CenteredVertically, animated: false)
profileVC.collectionView?.layoutIfNeeded() //This is necessary to force the collectionView to layout any necessary new cells after scrolling
cell = profileVC.collectionView?.cellForItemAtIndexPath(indexPath)
}
let imageSnapshot = horizontalFeedVC.view.snapshotViewAfterScreenUpdates(false)
let snapshotFrame = containerView.convertRect(horizontalFeedVC.view.frame, fromView: horizontalFeedVC.view)
imageSnapshot.frame = snapshotFrame
horizontalFeedVC.view.hidden = true
profileVC.view.frame = transitionContext.finalFrameForViewController(profileVC)
containerView.insertSubview(profileVC.view, belowSubview: horizontalFeedVC.view)
containerView.addSubview(imageSnapshot)
UIView.animateWithDuration(duration, animations: {
var cellFrame = CGRectMake(0, 0, 0, 0)
if let theFrame = cell?.frame {
cellFrame = theFrame
}
let frame = containerView.convertRect(cellFrame, fromView: profileVC.collectionView)
imageSnapshot.frame = frame
}, completion: { (succeed) in
if succeed {
horizontalFeedVC.view.hidden = false
// cell.contentView.hidden = false
imageSnapshot.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
})
}
}
}
Now if the current UICollectionViewCell is not yet created it:
Scrolls so the indexPath of the cell is centered on the view
Forces the UICollectionView to layout its subviews which creates any UITableViewCells that are necessary
Asks for the UITableViewCell again, but this time it should not be nil

Why does modifying this CALayer within a UITableViewCell cause a nasty graphics glitch?

In my cellForRowAtIndexPath method I set the cornerRadius of the layer on my image:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("feedCell")!
if self.pictunes?.count > 0 {
// There are some pictunes to show; Create and update the posts
if self.pictuneImages.count > indexPath.row {
// We have an image for the user who made the current pictune
if let pictunerImageView = cell.contentView.viewWithTag(2) as? UIImageView {
pictunerImageView.layer.cornerRadius = 17.5 // Cut it to a circle
pictunerImageView.image = self.pictunerImages[0]
}
// We also have an image for the pictune at the current post
if let pictuneImageView = cell.contentView.viewWithTag(1) as? UIImageView {
pictuneImageView.image = self.pictuneImages[indexPath.row]
}
}
return cell
} else {
// No pictunes have loaded yet; We probably need more time
// to load the image and audio assets for some of them
cell.textLabel!.text = "No Pictunes Available Yet"
return cell
}
}
But when I scroll I see this nasty background effect:
How should I get rid of this effect? Clearing the background context didn't help at all. Thanks!
Set:
pictunerImageView.layer.masksToBounds = true
after:
pictunerImageView.layer.cornerRadius = 17.5
You could also change this to:
pictunerImageView.layer.cornerRadius = pictunerImageView.frame.size.height *0.5
In case you ever want to change the size but still want it to be a circle.

UITableView Drag & Drop Outside Table = Crash

The Good
My drag & drop function almost works wonderfully. I longPress a cell and it smoothly allows me to move the pressed cell to a new location between two other cells. The table adjusts and the changes save to core data. Great!
The Bad
My problem is that if I drag the cell below the bottom cell in the table, even if I don't let go (un-press) of the cell... the app crashes. If I do the drag slowly, really it crashes as the cell crosses the y-center of the last cell... so I do think it's a problem related to the snapshot getting a location. Less important, but possibly related, is that if I long press below the last cell with a value in it, it also crashes.
The drag/drop runs off a switch statement that runs one of three sets of code based on the status:
One case when the press begins
One case when the cell is being dragged
One case when when the user lets go of the cell
My code is adapted from this tutorial:
Drag & Drop Tutorial
My code:
func longPressGestureRecognized(gestureRecognizer: UIGestureRecognizer) {
let longPress = gestureRecognizer as! UILongPressGestureRecognizer
let state = longPress.state
var locationInView = longPress.locationInView(tableView)
var indexPath = tableView.indexPathForRowAtPoint(locationInView)
struct My {
static var cellSnapshot : UIView? = nil
}
struct Path {
static var initialIndexPath : NSIndexPath? = nil
}
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! CustomTableViewCell;
var dragCellName = currentCell.nameLabel!.text
var dragCellDesc = currentCell.descLabel.text
//Steps to take a cell snapshot. Function to be called in switch statement
func snapshotOfCell(inputView: UIView) -> UIView {
UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, false, 0.0)
inputView.layer.renderInContext(UIGraphicsGetCurrentContext())
let image = UIGraphicsGetImageFromCurrentImageContext() as UIImage
UIGraphicsEndImageContext()
let cellSnapshot : UIView = UIImageView(image: image)
cellSnapshot.layer.masksToBounds = false
cellSnapshot.layer.cornerRadius = 0.0
cellSnapshot.layer.shadowOffset = CGSizeMake(-5.0, 0.0)
cellSnapshot.layer.shadowRadius = 5.0
cellSnapshot.layer.shadowOpacity = 0.4
return cellSnapshot
}
switch state {
case UIGestureRecognizerState.Began:
//Calls above function to take snapshot of held cell, animate pop out
//Run when a long-press gesture begins on a cell
if indexPath != nil && indexPath != nil {
Path.initialIndexPath = indexPath
let cell = tableView.cellForRowAtIndexPath(indexPath!) as UITableViewCell!
My.cellSnapshot = snapshotOfCell(cell)
var center = cell.center
My.cellSnapshot!.center = center
My.cellSnapshot!.alpha = 0.0
tableView.addSubview(My.cellSnapshot!)
UIView.animateWithDuration(0.25, animations: { () -> Void in
center.y = locationInView.y
My.cellSnapshot!.center = center
My.cellSnapshot!.transform = CGAffineTransformMakeScale(1.05, 1.05)
My.cellSnapshot!.alpha = 0.98
cell.alpha = 0.0
}, completion: { (finished) -> Void in
if finished {
cell.hidden = true
}
})
}
case UIGestureRecognizerState.Changed:
if My.cellSnapshot != nil && indexPath != nil {
//Runs when the user "lets go" of the cell
//Sets CG Y-Coordinate of snapshot cell to center of current location in table (snaps into place)
var center = My.cellSnapshot!.center
center.y = locationInView.y
My.cellSnapshot!.center = center
var appDel: AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
var context: NSManagedObjectContext = appDel.managedObjectContext!
var fetchRequest = NSFetchRequest(entityName: currentListEntity)
let sortDescriptor = NSSortDescriptor(key: "displayOrder", ascending: true )
fetchRequest.sortDescriptors = [ sortDescriptor ]
//If the indexPath is not 0 AND is not the same as it began (didn't move)...
//Update array and table row order
if ((indexPath != nil) && (indexPath != Path.initialIndexPath)) {
swap(&taskList_Cntxt[indexPath!.row], &taskList_Cntxt[Path.initialIndexPath!.row])
tableView.moveRowAtIndexPath(Path.initialIndexPath!, toIndexPath: indexPath!)
toolBox.updateDisplayOrder()
context.save(nil)
Path.initialIndexPath = indexPath
}
}
default:
if My.cellSnapshot != nil && indexPath != nil {
//Runs continuously while a long press is recognized (I think)
//Animates cell movement
//Completion block:
//Removes snapshot of cell, cleans everything up
let cell = tableView.cellForRowAtIndexPath(Path.initialIndexPath!) as UITableViewCell!
cell.hidden = false
cell.alpha = 0.0
UIView.animateWithDuration(0.25, animations: { () -> Void in
My.cellSnapshot!.center = cell.center
My.cellSnapshot!.transform = CGAffineTransformIdentity
My.cellSnapshot!.alpha = 0.0
cell.alpha = 1.0
}, completion: { (finished) -> Void in
if finished {
Path.initialIndexPath = nil
My.cellSnapshot!.removeFromSuperview()
My.cellSnapshot = nil
}
})//End of competion block & end of animation
}//End of 'if nil'
}//End of switch
}//End of longPressGestureRecognized
Potential Culprit
My guess is that the issue is related to the cell being unable to get coordinates once it is below the last cell. It isn't really floating, it is constantly setting its location in relation to the other cells. I think the solution will be an if-statement that does something magical when there's no cell to reference for a location. But what!?! Adding a nil check to each case isn't working for some reason.
Clearly Stated Question
How do I avoid crashes and handle an event where my dragged cell is dragged below the last cell?
Screenshot of crash:
The Ugly
It seems that you simply need to do a preemptive check, to ensure your indexPath is not nil:
var indexPath = tableView.indexPathForRowAtPoint(locationInView)
if (indexPath != nil) {
//Move your code to this block
}
Hope that helps!
You don't state where in the code the crash occurs, which makes it harder to determine what is going on. Set a breakpoint on exceptions to determine which line is the culprit. To do that, use the '+' in the bottom-left corner of the breakpoint list in XCode.
The main issue I think is with the indexPath. There are a couple of issues:
You are using the indexPath even though it might be nil, in this line:
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! CustomTableViewCell;
The indexPath can be invalid, even though it is not nil. Check for its section and row members to be different from NSNotFound.
Finally, I have been using a pre-made, open source, UITableView subclass that does all the moving for you, so you don't have to implement it yourself anymore. It also takes care of autoscrolling, which you have not even considered yet. Use it directly, or use it as inspiration for your code:
https://www.cocoacontrols.com/controls/fmmovetableview

use Alamofire to show image in collectionView in ios

I use Alamofire library for showing image in cell in the collectionView
but my problem is when I scrolling up && down , my CollectionView showing wrong image in cell
and this is my snippet code for set cell data
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CardViewCell
let pos = indexPath.item % 5
var paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 2.5
paragraphStyle.alignment = .Right
paragraphStyle.lineBreakMode = .ByTruncatingTail
if let text = currentCollection.titles?[indexPath.item] {
var attrString = NSMutableAttributedString(string: text)
attrString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range:NSMakeRange(0, attrString.length))
cell.title.attributedText = attrString
} else {
cell.title.text = currentCollection.titles?[indexPath.item]
}
if let catId = currentCollection.catIds?[indexPath.item] {
cell.iconImage.image = UIImage(named: icons[catId-1])
}
cell.title.font = UIFont(name: "B Yekan", size: 14)
cell.title.preferredMaxLayoutWidth = cell.frame.size.width - 48
cell.iconImage.image = cell.iconImage.image!.imageWithRenderingMode(.AlwaysTemplate)
cell.iconImage.tintColor = UIColor.whiteColor()
let imageUrl = currentCollection.urlImages![indexPath.item]
if let image = currentCollection.imageCache.objectForKey(imageUrl) as? UIImage {
cell.backImage.image = image
} else {
cell.backImage.image = nil
cell.request = Alamofire.request(.GET, imageUrl).validate(contentType: ["image/*"]).responseImage() {
(request, _, image, error) in
if error == nil && image != nil {
self.currentCollection.imageCache.setObject(image!, forKey: request.URLString)
cell.backImage.image = image
}
}
}
cell.layer.shadowPath = UIBezierPath(roundedRect: cell.bounds, cornerRadius: cell.layer.cornerRadius).CGPath
if indexPath.item == currentCollection.titles!.count-3 {
currentCollection.page++
appendTitlesInPage(currentCollection.page)
}
return cell
}
can help me where is the wrong? please!
The image request takes time - during that time the request keeps a reference to the cell, and sets the cell's backImage.image once the request is complete. Unfortunately, by that time, the cell may have been scrolled off-screen and may be being re-used at another indexPath, where the image is incorrect. Instead, you should use the table view's cellForItemAtIndexPath method to get the correct cell (if the cell is no longer visible, this will return nil which will prevent your error).
EDIT: Now I have developed a small CocoaPods library to handle this problem seamlessly. I suggest you to use this one, I have it running in a couple of projects myself without any issues.
https://github.com/gchiacchio/AlamoImage
As #johnpatrickmorgan said, the problem is: when you scroll the cell is reused, and by the time the request for the image is responded, it's no longer valid (image & cell don't match).
As the response time is determined by networking conditions, you only can wait for the response, but what you CAN DO is to CANCEL the request in progress at the moment you know the cell will be reused. To accomplish that you can put a cell.request?.cancel() BEFORE setting the new image, as follows:
let imageUrl = currentCollection.urlImages![indexPath.item]
//Cancel the request in progress (to a wrong image) if any.
cell.request?.cancel()
if let image = currentCollection.imageCache.objectForKey(imageUrl) as? UIImage {
cell.backImage.image = image
} else {
cell.backImage.image = nil
cell.request = Alamofire.request(.GET, imageUrl).validate(contentType: ["image/*"]).responseImage() {
(request, _, image, error) in
if error == nil && image != nil {
self.currentCollection.imageCache.setObject(image!, forKey: request.URLString)
cell.backImage.image = image
}
}
}
Look at the "LazyTableImages" sample app that Apple provides. It's with a UITableView, but it would be essentially the same code as with a UICollectionView, and you don't need a third party library to do it.
IconDownloader *iconDownloader = (self.imageDownloadsInProgress)[indexPath];
if (iconDownloader == nil)
{
iconDownloader = [[IconDownloader alloc] init];
iconDownloader.appRecord = appRecord;
[iconDownloader setCompletionHandler:^{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
// Display the newly loaded image
cell.imageView.image = appRecord.appIcon;
// Remove the IconDownloader from the in progress list.
// This will result in it being deallocated.
[self.imageDownloadsInProgress removeObjectForKey:indexPath];
}];
(self.imageDownloadsInProgress)[indexPath] = iconDownloader;
[iconDownloader startDownload];
The relevant part to you is in the completion handler, where you're getting the cell using cellForRowAtIndexPath. This (and the corresponding collection view method) ensure you're getting the right cell, and if it's offscreen it returns nil and everything degrades gracefully.

Resources