I have a markerview (uiview) that forever and repeatedly goes through a bunch of uicollectionviewcells I want to detect and possibly change the colour of those cells detected and reset it back when the marker goes past the cell.
Heres what i have so far :
View did appear :
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(displayLinkHandler)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
- (void) displayLinkHandler {
id presentationLayer1 = marker.markerLine.layer.presentationLayer;
for (NSIndexPath* cellPath in [seqcollectionview indexPathsForVisibleItems]) {
UICollectionViewCell *cell = [seqcollectionview cellForItemAtIndexPath:cellPath];
id presentationLayer2 = cell.layer.presentationLayer;
BOOL nowIntersecting = CGRectIntersectsRect([presentationLayer1 frame], [presentationLayer2 frame]);
// I'll keep track of whether the two views were intersected in a class property,
// and therefore only display a message if the intersected state changes.
if (nowIntersecting != wasIntersected) {
if (nowIntersecting && cell.selected) {
NSLog(#"row is: %d section is: %d", [cellPath row], [cellPath section]);
cell.backgroundColor = [UIColor whiteColor];
}else{
cell.backgroundColor = [UIColor colorWithRed:1 green:0.855 blue:0.741 alpha:1];
}
wasIntersected = nowIntersecting;
}
}
}
So far this works but the delay for when the color is meant to change is just too much !
Cheers
Related
I have a UICollectionView.I am trying to give it as SpringBoard functionality.I have am able to give the shake animation to each cell.But i want when icons are shaking then i should be able to move them as well.
For shaking the cells i have added the UILongPressGesture on each cell.When gesture end then i have added one custom animation on them & also added a delete button on top left corner.
Code for long press gesture:
declaration of variables
CGPoint p;
UILongPressGestureRecognizer *lpgr;
NSIndexPath *gesture_indexPath;
add gesture to collection view
lpgr
= [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleLongPress:)];
lpgr.minimumPressDuration = .3; // To detect after how many seconds you want shake the cells
lpgr.delegate = self;
[self.collection_view addGestureRecognizer:lpgr];
lpgr.delaysTouchesBegan = YES;
Callback method
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer.state != UIGestureRecognizerStateEnded)
{
return;
}
p = [gestureRecognizer locationInView:self.collection_view];
NSIndexPath *indexPath = [self.collection_view indexPathForItemAtPoint:p];
if (indexPath == nil)
{
NSLog(#"couldn't find index path");
}
else
{
[[NSUserDefaults standardUserDefaults]setValue:#"yes" forKey:#"longPressed"];
[self.collection_view reloadData];
}
}
Cell for Item at inde path
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"arr_album index row");
BlogAlbumCell *cell;
static NSString *identifier = #"UserBlogAlbum";
cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
UserAlbum *user_allbum=[arr_userAlbums objectAtIndex:indexPath.row];
cell.label_blog_name.text=user_allbum.album_name;
cell.image_blog_image.image = [UIImage imageNamed:#"more.png"];
[cell.image_blog_image setImageWithURL:[NSURL URLWithString:[IMAGE_BASE_URL stringByAppendingString:user_allbum.album_image]]];
if([[[NSUserDefaults standardUserDefaults]valueForKey:#"longPressed"] isEqualToString:#"yes"])
{
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
[anim setToValue:[NSNumber numberWithFloat:0.0f]];
[anim setFromValue:[NSNumber numberWithDouble:M_PI/50]];
[anim setDuration:0.1];
[anim setRepeatCount:NSUIntegerMax];
[anim setAutoreverses:YES];
cell.layer.shouldRasterize = YES;
[cell.layer addAnimation:anim forKey:#"SpringboardShake"];
CGFloat delButtonSize = 20;
UIButton *delButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, delButtonSize, delButtonSize)];
delButton.center = CGPointMake(9, 10);
delButton.backgroundColor = [UIColor clearColor];
[delButton setImage: [UIImage imageNamed:#"cross_30.png"] forState:UIControlStateNormal];
[cell addSubview:delButton];
[delButton addTarget:self action:#selector(deleteRecipe:) forControlEvents:UIControlEventTouchUpInside];
}
else if ([[[NSUserDefaults standardUserDefaults]valueForKey:#"singleTap"] isEqualToString:#"yes"])
{
for(UIView *subview in [cell subviews])
{
if([subview isKindOfClass:[UIButton class]])
{
[subview removeFromSuperview];
}
else
{
// Do nothing - not a UIButton or subclass instance
}
}
[cell.layer removeAllAnimations];
// _deleteButton.hidden = YES;
// [_deleteButton removeFromSuperview];
}
return cell;
}
It works fine till here.
For moving the cell i made a sample app in which i added UICollectionViewController & override this method
-(void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
NSLog(#"Move at index path called");
}
This also works fine.It also uses long press gesture & when gesture detetced then i am able to move the cells.But now the issue is at one either i can move cell or animate them.If i add my custom gesture then i am not able to move the images.Please tell me how can i remove this issue?
Take a look at this project called DragDropCollectionView. It implements the dragging and dropping as well as the animation.
Edit: This problem can be broken down into 2 smaller subproblems:
How to animate the cells
How to reorder the cells using the drag and drop.
You should combine these 2 solutions into a subclas of UICollectionView to obtain your main solution.
How to animate the cells
To get the wiggle animation you need add 2 different animation effects:
Move the cells vertically i.e up and down bounce
Rotate the cells
Lastly, add a random interval time for each cell so it does appear that the cells don't animate uniformly
Here is the code:
#interface DragDropCollectionView ()
#property (assign, nonatomic) BOOL isWiggling;
#end
#implementation DragDropCollectionView
//Start and Stop methods for wiggle
- (void) startWiggle {
for (UICollectionViewCell *cell in self.visibleCells) {
[self addWiggleAnimationToCell:cell];
}
self.isWiggling = true;
}
- (void)stopWiggle {
for (UICollectionViewCell *cell in self.visibleCells) {
[cell.layer removeAllAnimations];
}
self.isWiggling = false;
}
//- (UICollectionViewCell *)dequ
- (UICollectionViewCell *)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(nonnull NSIndexPath *)indexPath{
UICollectionViewCell *cell = [super dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
if (self.isWiggling) {
[self addWiggleAnimationToCell:cell];
} else {
[cell.layer removeAllAnimations];
}
return [[UICollectionViewCell alloc] init];
}
//Animations
- (void)addWiggleAnimationToCell:(UICollectionViewCell *)cell {
[CATransaction begin];
[CATransaction setDisableActions:false];
[cell.layer addAnimation:[self rotationAnimation] forKey:#"rotation"];
[cell.layer addAnimation:[self bounceAnimation] forKey:#"bounce"];
[CATransaction commit];
}
- (CAKeyframeAnimation *)rotationAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation.z"];
CGFloat angle = 0.04;
NSTimeInterval duration = 0.1;
double variance = 0.025;
animation.values = #[#(angle), #(-1 * angle)];
animation.autoreverses = YES;
animation.duration = [self randomizeInterval:duration withVariance: variance];
animation.repeatCount = INFINITY;
return animation;
}
- (CAKeyframeAnimation *)bounceAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform.translation.y"];
CGFloat bounce = 3.0;
NSTimeInterval duration = 0.12;
double variance = 0.025;
animation.values = #[#(bounce), #(-1 * bounce)];
animation.autoreverses = YES;
animation.duration = [self randomizeInterval:duration withVariance: variance];
animation.repeatCount = INFINITY;
return animation;
}
- (NSTimeInterval)randomizeInterval:(NSTimeInterval)interval withVariance:(double)variance {
double randomDecimal = (arc4random() % 1000 - 500.0) / 500.0;
return interval + variance * randomDecimal;
}
How to reorder the cells using drag and drop
So the idea is this: You’re not moving the actual cell around, but rather moving a UIImageView with a UIImage of the contents of the cell.
The algorithm goes more or less like this. I’ve broken it down into 3 sections, gestureRecognizerBegan, Changed and Ended
gestureRecognizerBegan:
When the gestureRecognizer begins, determine if the long press was indeed on a cell (and not on empty space)
Get a UIImage of the cell (See my method “getRasterizedImageOfCell”)
Hide the cell (i.e alpha = 0), create a UIImageView with the exact frame of the cell so the user does not realize you’ve actually hidden the cell and you’re actually using the imageview.
gestureRecognizerChanged:
Update the center of the UIImageView so that it moves with your finger.
If the user has stopped moving his finge i.e. he is hovering over the cell he wants to replace, you now need to swap the cells. (Look at my function “shouldSwapCells”, this method returns a bool of whether the cells should swap or not)
Move the cell that you were dragging to the new indexPath. (Look at my method “swapDraggedCell”). UICollectionView has a built-in method called "moveItemAtIndexPath: toIndexPath”, I’m not sure if UITableView has the same thing
gestureRecognizerEnd:
“Drop” the UIImageView back onto the cell
change the cell alpha from 0.0 to 1.0 and remove the UIImageView from the view.
Here is the code:
#interface DragDropCollectionView ()
#property (strong, nonatomic) NSIndexPath *draggedCellIndexPath;
#property (strong, nonatomic) UIImageView *draggingImageView;
#property (assign, nonatomic) CGPoint touchOffsetFromCenterOfCell;
#property (strong, nonatomic) UILongPressGestureRecognizer *longPressRecognizer;
#end
#implementation DragDropCollectionView
- (void)handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer {
CGPoint touchLocation = [longPressRecognizer locationInView:self];
switch (longPressRecognizer.state) {
case UIGestureRecognizerStateBegan: {
self.draggedCellIndexPath = [self indexPathForItemAtPoint:touchLocation];
if (self.draggedCellIndexPath != nil) {
UICollectionViewCell *draggedCell = [self cellForItemAtIndexPath:self.draggedCellIndexPath];
self.draggingImageView = [[UIImageView alloc] initWithImage:[self rasterizedImageCopyOfCell:draggedCell]];
self.draggingImageView.center = draggedCell.center;
[self addSubview:self.draggingImageView];
draggedCell.alpha = 0.0;
self.touchOffsetFromCenterOfCell = CGPointMake(draggedCell.center.x - touchLocation.x, draggedCell.center.y - touchLocation.y);
[UIView animateWithDuration:0.4 animations:^{
self.draggingImageView.transform = CGAffineTransformMakeScale(1.3, 1.3);
self.draggingImageView.alpha = 0.8;
}];
}
break;
}
case UIGestureRecognizerStateChanged: {
if (self.draggedCellIndexPath != nil) {
self.draggingImageView.center = CGPointMake(touchLocation.x + self.touchOffsetFromCenterOfCell.x, touchLocation.y + self.touchOffsetFromCenterOfCell.y);
}
float pingInterval = 0.3;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(pingInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSIndexPath *newIndexPath = [self indexPathToSwapCellWithAtPreviousTouchLocation:touchLocation];
if (newIndexPath) {
[self swapDraggedCellWithCellAtIndexPath:newIndexPath];
}
});
break;
}
case UIGestureRecognizerStateEnded: {
if (self.draggedCellIndexPath != nil ) {
UICollectionViewCell *draggedCell = [self cellForItemAtIndexPath:self.draggedCellIndexPath];
[UIView animateWithDuration:0.4 animations:^{
self.draggingImageView.transform = CGAffineTransformIdentity;
self.draggingImageView.alpha = 1.0;
if (draggedCell != nil) {
self.draggingImageView.center = draggedCell.center;
}
} completion:^(BOOL finished) {
[self.draggingImageView removeFromSuperview];
self.draggingImageView = nil;
if (draggedCell != nil) {
draggedCell.alpha = 1.0;
self.draggedCellIndexPath = nil;
}
}];
}
}
default:
break;
}
}
- (UIImage *)rasterizedImageCopyOfCell:(UICollectionViewCell *)cell {
UIGraphicsBeginImageContextWithOptions(cell.bounds.size, false, 0.0);
[cell.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
return image;
}
- (NSIndexPath *)indexPathToSwapCellWithAtPreviousTouchLocation:(CGPoint)previousTouchLocation {
CGPoint currentTouchLocation = [self.longPressRecognizer locationInView:self];
if (!isnan(currentTouchLocation.x) && !isnan(currentTouchLocation.y)) {
if ([self distanceBetweenPoints:currentTouchLocation secondPoint:previousTouchLocation] < 20.0) {
NSIndexPath *newIndexPath = [self indexPathForItemAtPoint:currentTouchLocation];
return newIndexPath;
}
}
return nil;
}
- (CGFloat)distanceBetweenPoints:(CGPoint)firstPoint secondPoint:(CGPoint)secondPoint {
CGFloat xDistance = firstPoint.x - secondPoint.x;
CGFloat yDistance = firstPoint.y - secondPoint.y;
return sqrtf(xDistance * xDistance + yDistance * yDistance);
}
- (void)swapDraggedCellWithCellAtIndexPath:(NSIndexPath *)newIndexPath {
[self moveItemAtIndexPath:self.draggedCellIndexPath toIndexPath:newIndexPath];
UICollectionViewCell *draggedCell = [self cellForItemAtIndexPath:newIndexPath];
draggedCell.alpha = 0.0;
self.draggedCellIndexPath = newIndexPath;
}
Hope this helps :)
Simple question:
Apple Music's table views only show an A-Z section index on the right edge once they've been scrolled down past a certain threshold, and that index animates in and out nicely.
I've been able to trigger the appearing / disappearing behaviour with the code below, but the index just pops in and out, there's no animation, and I can't find any way to get one to show up.
func sectionIndexTitlesForTableView(tableView: UITableView) -> [AnyObject]! {
if tableView.contentOffset.y > 88 {
return DataManager.sharedManager.frc!.sectionIndexTitles
} else {
return []
}
}
func scrollViewDidScroll(scrollView: UIScrollView) {
tableView.reloadSectionIndexTitles()
}
This basically means each time the scroll view ticks, it'll reload the section indexes, then conditionally hide or show the index based on the offset of the table. As I say, it works, but it doesn't animate the index, and I'd really love that functionality if possible.
Seems like it is not possible to do that, ate least apple doesn't provide any API to animate the section index view. I am able to slide in/out the indexes, but it doesn't resize the cells' contentView.
When you animate the section index view, right after the animation it comes back to its initial position. So I am basically setting the color to clearColor/black when hidden/visible.
I am not sure if apple approves this code since it's kind of using undocumented APIs
- (UIView *)indexTitlesView {
NSArray *subViews = [self.tableView subviews];
for (UIView *view in subViews) {
if ([NSStringFromClass([view class]) isEqualToString:#"UITableViewIndex"]) {
return view;
}
}
return nil;
}
- (void)slideIndexTitlesViewIn {
UIView *indexTitlesView = [self indexTitlesView];
CABasicAnimation *contentPositionAnimation = [CABasicAnimation animationWithKeyPath:#"position.x"];
contentPositionAnimation.fromValue = #(CGRectGetWidth(indexTitlesView.frame));
contentPositionAnimation.toValue = #(0);
contentPositionAnimation.additive = YES;
contentPositionAnimation.duration = 0.3;
contentPositionAnimation.delegate = self;
contentPositionAnimation.removedOnCompletion = NO;
contentPositionAnimation.fillMode = kCAFillModeForwards;
[indexTitlesView.layer removeAllAnimations];
[indexTitlesView.layer addAnimation:contentPositionAnimation forKey:#"slideInAnimation"];
self.tableView.sectionIndexColor = [UIColor blackColor];
}
- (void)slideIndexTitlesViewOut {
UIView *indexTitlesView = [self indexTitlesView];
CABasicAnimation *contentPositionAnimation = [CABasicAnimation animationWithKeyPath:#"position.x"];
contentPositionAnimation.fromValue = #(0);
contentPositionAnimation.toValue = #(CGRectGetWidth(indexTitlesView.frame));
contentPositionAnimation.additive = YES;
contentPositionAnimation.duration = 0.3;
contentPositionAnimation.delegate = self;
contentPositionAnimation.removedOnCompletion = NO;
contentPositionAnimation.fillMode = kCAFillModeForwards;
[indexTitlesView.layer removeAllAnimations];
[indexTitlesView.layer addAnimation:contentPositionAnimation forKey:#"slideOutAnimation"];
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
UIView *indexTitlesView = [self indexTitlesView];
NSArray *keys = [indexTitlesView.layer animationKeys];
for (NSString *key in keys) {
if ([indexTitlesView.layer animationForKey:key] == anim) {
if ([key isEqualToString:#"slideOutAnimation"] && flag == YES) {
self.tableView.sectionIndexColor = [UIColor clearColor];
}
[indexTitlesView.layer removeAllAnimations];
return;
}
}
}
Just use the animateWithDuration() method set the alpha value to zero and then animate it in by setting the value to 1
I would not suggest playing with private classes, since it can easily break in a future OS release.
I've implemented a custom control for the exact purpose. It mimics the native table index appearance while providing much more customization capabilities.
Check https://github.com/mindz-eye/MYTableViewIndex for details.
Background
I have searched around SO and apple forum. Quite a lot of people talked about performance of collection view cell with image. Most of them said it is lag on scroll since loading the image in the main thread.
By using SDWebImage, the images should be loading in separate thread. However, it is lag only in the landscape mode in the iPad simulator.
Problem description
In the portrait mode, the collection view load 3 cells for each row. And it has no lag or insignificant delay.
In the landscape mode, the collection view load 4 cells for each row. And it has obvious lag and drop in frame rate.
I have checked with instrument tools with the core animation. The frame rate drop to about 8fps when new cell appear. I am not sure which act bring me such a low performance for the collection view.
Hope there would be someone know the tricks part.
Here are the relate code
In The View Controller
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ProductCollectionViewCell *cell=[collectionView dequeueReusableCellWithReuseIdentifier:#"ProductViewCell" forIndexPath:indexPath];
Product *tmpProduct = (Product*)_ploader.loadedProduct[indexPath.row];
cell.product = tmpProduct;
if (cellShouldAnimate) {
cell.alpha = 0.0;
[UIView animateWithDuration:0.2
delay:0
options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction)
animations:^{
cell.alpha = 1.0;
} completion:nil];
}
if(indexPath.row >= _ploader.loadedProduct.count - ceil((LIMIT_COUNT * 0.3)))
{
[_ploader loadProductsWithCompleteBlock:^(NSError *error){
if (nil == error) {
cellShouldAnimate = NO;
[_collectionView reloadData];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
cellShouldAnimate = YES;
});
} else if (error.code != 1){
#ifdef DEBUG_MODE
ULog(#"Error.des : %#", error.description);
#else
CustomAlertView *alertView = [[CustomAlertView alloc]
initWithTitle:#"Connection Error"
message:#"Please retry."
buttonTitles:#[#"OK"]];
[alertView show];
#endif
}
}];
}
return cell;
}
PrepareForReuse in the collectionViewCell
- (void)prepareForReuse
{
[super prepareForReuse];
CGRect bounds = self.bounds;
[_thumbnailImgView sd_cancelCurrentImageLoad];
CGFloat labelsTotalHeight = bounds.size.height - _thumbnailImgView.frame.size.height;
CGFloat brandToImageOffset = 2.0;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
brandToImageOffset = 53.0;
}
CGFloat labelStartY = _thumbnailImgView.frame.size.height + _thumbnailImgView.frame.origin.y + brandToImageOffset;
CGFloat nameLblHeight = labelsTotalHeight * 0.46;
CGFloat priceLblHeight = labelsTotalHeight * 0.18;
_brandLbl.frame = (CGRect){{15, labelStartY}, {bounds.size.width - 30, nameLblHeight}};
CGFloat priceToNameOffset = 8.0;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
priceToNameOffset = 18.0;
}
_priceLbl.frame = (CGRect){{5, labelStartY + nameLblHeight - priceToNameOffset}, {bounds.size.width-10, priceLblHeight}};
[_spinner stopAnimating];
[_spinner removeFromSuperview];
_spinner = nil;
}
Override the setProduct method
- (void)setProduct:(Product *)product
{
_product = product;
_spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
_spinner.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
[self addSubview:_spinner];
[_spinner startAnimating];
_spinner.hidesWhenStopped = YES;
// Add a spinner
__block UIActivityIndicatorView *tmpSpinner = _spinner;
__block UIImageView *tmpImgView = _thumbnailImgView;
ProductImage *thumbnailImage = _product.images[0];
[_thumbnailImgView sd_setImageWithURL:[NSURL URLWithString:thumbnailImage.mediumURL]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
// dismiss the spinner
[tmpSpinner stopAnimating];
[tmpSpinner removeFromSuperview];
tmpSpinner = nil;
if (nil == error) {
// Resize the incoming images
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGFloat imageHeight = image.size.height;
CGFloat imageWidth = image.size.width;
CGSize newSize = tmpImgView.bounds.size;
CGFloat scaleFactor = newSize.width / imageWidth;
newSize.height = imageHeight * scaleFactor;
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *small = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(),^{
tmpImgView.image = small;
});
});
if (cacheType == SDImageCacheTypeNone) {
tmpImgView.alpha = 0.0;
[UIView animateWithDuration:0.2
delay:0
options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction)
animations:^{
tmpImgView.alpha = 1.0;
} completion:nil];
}
} else {
// loading error
[tmpImgView setImage:[UIImage imageNamed:#"broken_image_small"]];
}
}];
_brandLbl.text = [_product.brand.name uppercaseString];
_nameLbl.text = _product.name;
[_nameLbl sizeToFit];
// Format the price
NSNumberFormatter * floatFormatter = [[NSNumberFormatter alloc] init];
[floatFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
[floatFormatter setDecimalSeparator:#"."];
[floatFormatter setMaximumFractionDigits:2];
[floatFormatter setMinimumFractionDigits:0];
[floatFormatter setGroupingSeparator:#","];
_priceLbl.text = [NSString stringWithFormat:#"$%# USD", [floatFormatter stringFromNumber:_product.price]];
if (_product.salePrice.intValue > 0) {
NSString *rawStr = [NSString stringWithFormat:#"$%# $%# USD", [floatFormatter stringFromNumber:_product.price], [floatFormatter stringFromNumber:_product.salePrice]];
NSMutableAttributedString * string = [[NSMutableAttributedString alloc] initWithString:rawStr];
// Change all the text to red first
[string addAttribute:NSForegroundColorAttributeName
value:[UIColor colorWithRed:157/255.0 green:38/255.0 blue:29/255.0 alpha:1.0]
range:NSMakeRange(0,rawStr.length)];
// find the first space
NSRange firstSpace = [rawStr rangeOfString:#" "];
// Change from zero to space to gray color
[string addAttribute:NSForegroundColorAttributeName
value:_priceLbl.textColor
range:NSMakeRange(0, firstSpace.location)];
[string addAttribute:NSStrikethroughStyleAttributeName
value:#2
range:NSMakeRange(0, firstSpace.location)];
_priceLbl.attributedText = string;
}
}
SDWebImage is very admirable, but DLImageLoader is absolutely incredible, and a key piece of many big production apps
https://stackoverflow.com/a/19115912/294884
it's amazingly easy to use.
To avoid the skimming problem, basically just introduce a delay before bothering to start downloading the image. So, essentially like this...it's this simple
dispatch_after_secs_on_main(0.4, ^
{
if ( ! [urlWasThen isEqualToString:self.currentImage] )
{
// so in other words, in fact, after a short period of time,
// the user has indeed scrolled away from that item.
// (ie, the user is skimming)
// this item is now some "new" item so of course we don't
// bother loading "that old" item
// ie, we now know the user was simply skimming over that item.
// (just TBC in the preliminary clause above,
// since the image is already in cache,
// we'd just instantly load the image - even if the user is skimming)
// NSLog(#" --- --- --- --- --- --- too quick!");
return;
}
// a short time has passed, and indeed this cell is still "that" item
// the user is NOT skimming, SO we start loading the image.
//NSLog(#" --- not too quick ");
[DLImageLoader loadImageFromURL:urlWasThen
completed:^(NSError *error, NSData *imgData)
{
if (self == nil) return;
// some time has passed while the image was loading from the internet...
if ( ! [urlWasThen isEqualToString:self.currentImage] )
{
// note that this is the "normal" situation where the user has
// moved on from the image, so no need toload.
//
// in other words: in this case, not due to skimming,
// but because SO much time has passed,
// the user has moved on to some other part of the table.
// we pointlessly loaded the image from the internet! doh!
//NSLog(#" === === 'too late!' image load!");
return;
}
UIImage *image = [UIImage imageWithData:imgData];
self.someImage.image = image;
}];
});
That's the "incredibly easy" solution.
IMO, after vast experimentation, it actually works considerably better than the more complex solution of tracking when the scroll is skimming.
once again, DLImageLoader makes all this extremely easy https://stackoverflow.com/a/19115912/294884
Note that the section of code above is just the "usual" way you load an image inside a cell.
Here's typical code that would do that:
-(void)imageIsNow:(NSString *)imUrl
{
// call this routine o "set the image" on this cell.
// note that "image is now" is a better name than "set the image"
// Don't forget that cells very rapidly change contents, due to
// the cell reuse paradigm on iOS.
// this cell is being told that, the image to be displayed is now this image
// being aware of scrolling/skimming issues, cache issues, etc,
// utilise this information to apprporiately load/whatever the image.
self.someImage.image = nil; // that's UIImageView
self.currentImage = imUrl; // you need that string property
[self loadImageInASecIfItsTheSameAs:imUrl];
}
-(void)loadImageInASecIfItsTheSameAs:(NSString *)urlWasThen
{
// (note - at this point here the image may already be available
// in cache. if so, just display it. I have omitted that
// code for simplicity here.)
// so, right here, "possibly load with delay" the image
// exactly as shown in the code above .....
dispatch_after_secs_on_main(0.4, ^
...etc....
...etc....
}
Again this is all easily possible due to DLImageLoader which is amazing. It is an amazingly solid library.
- (IBAction)playShout:(UIButton *)sender {
self.tableView.userInteractionEnabled=NO;
CGPoint point = [sender.superview convertPoint:sender.frame.origin toView:self.tableView];
NSIndexPath *indexPath = self.tempIndexPath;//[self.tableView indexPathForRowAtPoint:point];
DisplayPersonTableViewCell * cell=[self.tableView cellForRowAtIndexPath:indexPath];
UIButton * button=cell.playButton;
self.tableView.userInteractionEnabled=YES;
PFObject * shoutToPlay=[self.shoutsArrayForUserBeingDisplayed objectAtIndex:indexPath.row];
[shoutToPlay fetchIfNeeded];
[AnimationViewController endCircleAnimation];
NSData * audioData=[self.audioDataDict objectForKey:shoutToPlay.objectId];
if (player.playing) {
if(player.data==audioData) {
[player stop];
[AnimationViewController endCircleAnimation];
} else{
[self setAllTableViewCellsToPlayButton];
[AnimationViewController endCircleAnimation];
sender.layer.cornerRadius=sender.frame.size.width/2;
player = [[AVAudioPlayer alloc] initWithData:audioData error:nil];
player.delegate=self;
[player prepareToPlay];
if (DISPLAY_IS_MUTED) {
player.volume=0;
}
[player play];
[AnimationViewController animateButton:cell.playButton ForDuration:player.duration withColor:[UIColor whiteColor]];
}
} else {
sender.layer.cornerRadius=sender.frame.size.width/2;
player = [[AVAudioPlayer alloc] initWithData:audioData error:nil];
player.delegate=self;
[player prepareToPlay];
if (DISPLAY_IS_MUTED) {
player.volume=0;
}
[player play];
[AnimationViewController animateButton:sender ForDuration:player.duration withColor:[UIColor whiteColor]];
}
}
Very simple and very easy, of course. But if I perform an animation on a button in that cell, and then scroll the tableview, it will switch and start animating the wrong cells. It's as if the fact that the superview's frame is changing affect's which button is being animated.
Any advice?
Animation method:
+(void)animateButton:(UIButton *)button ForDuration:(NSTimeInterval)duration withColor:(UIColor *)color {
circle = [CAShapeLayer layer];
circle.fillColor = nil;
circle.lineWidth = button.frame.size.height/2+2;
circle.strokeColor = color.CGColor;
circle.bounds = CGRectMake(0, 0, button.frame.size.width/2, button.frame.size.height/2);
circle.frame= CGRectMake(button.frame.size.width/4, button.frame.size.height/4, button.frame.size.width/2+2, button.frame.size.height/2+2);
circle.path = [UIBezierPath bezierPathWithOvalInRect:circle.bounds].CGPath;
[button.layer addSublayer:circle];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = duration;
drawAnimation.repeatCount = 1.0;
drawAnimation.removedOnCompletion = YES;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
Are you implementing the prepareForReuse method in your cell? There you must stop running animations and reset the state of the cell for the reuse.
From the docs:
prepareForReuse
Prepares a reusable cell for reuse by the table view's delegate.
If a UITableViewCell object is reusable—that is, it has a reuse identifier—this method is invoked just before the object is returned
from the UITableView method dequeueReusableCellWithIdentifier:. For
performance reasons, you should only reset attributes of the cell that
are not related to content, for example, alpha, editing, and selection
state. The table view's delegate in tableView:cellForRowAtIndexPath:
should always reset all content when reusing a cell. If the cell
object does not have an associated reuse identifier, this method is
not called. If you override this method, you must be sure to invoke
the superclass implementation.
You need to implement a custom UITableViewCell (create a subclass of it) and implement the prepareForReuse method.
There you can write the logic to stop the animation.
I am attempting to create a stack of photos using UICollectionView in which a user can swipe a photo to the side. As a photo is dragged, it gets smaller (to a minimum scale) and the photo behind is enlarged to a maximum scale. This maximum scale is the original size of the foreground photo.
When the pan gesture which drags the foreground photo ends, if it is the minimum scale, it is sent to the back of the stack, otherwise, it is animated back to the centre of the screen and enlarged back to its original size (along with the growing photo behind it).
I hope you're still with me haha.
Now, assuming the photo being dragged is at its minimum size, I remove user interaction of the UICollectionView, animate the photo to the centre of the screen again, and its size is animated to match the rest of the background images. I then send the cell to the back of the UICollectionViews subviews.
Once all of these animations have taken place, I update my data source (an array of images) by adding the photo in front to the back, then deleting that photo at the front.
Then, I update the UICollectionView. This consists of a batch update in which I delete the item at an NSIndexPath of 0 : 0, then, insert an item at NSIndexPath ([photo count] - 1) : 0.
When this batch update finished, I reload my UICollectionView (as only the item at NSIndexPath 0 : 0 has a pan gesture) and re-add user interaction to the UICollectionView.
I hope this makes sense.
Notes
The size of the background photos is a percent of 80%.
The size of the foreground photo is a percent of 100%.
This is relative to the UICollectionView which fits the screen of the iPhone.
My problem
My code seems to work very well. My issue comes from when I take my finger off the screen (the pan gesture ends) and the animation of the photo (centring it to the centre of the screen and sizing it like the rest of the background photos) ends.
The photo dragged off reappears on screen, sizes from the size of the background photos (80%) to the size of the foreground photo (100%) then fades away. Once this happens the photos are reordered as expected.
Does anyone know why this might happen?
Here is my Pan Gesture code:
CGPoint touchTranslation = [gesture translationInView:self.view];
static CGPoint originalCenter;
DIGalleryCollectionViewLayout *galleryCollectionViewLayout = (DIGalleryCollectionViewLayout *)self.galleryCollectionView.collectionViewLayout;
if (gesture.state == UIGestureRecognizerStateBegan) {
//
// Began.
//
originalCenter = gesture.view.center;
} else if (gesture.state == UIGestureRecognizerStateChanged) {
//
// Changed.
//
CGPoint translate = [gesture translationInView:gesture.view.superview];
touchTranslation = CGPointMake(originalCenter.x + translate.x, originalCenter.y + translate.y);
[gesture.view setCenter:touchTranslation];
CGPoint pointsPhotoIsMovedBy = CGPointMake(fabsf(fabsf(originalCenter.x) - fabsf(touchTranslation.x)),
fabsf(fabsf(originalCenter.y) - fabsf(touchTranslation.y)));
// Update cells.
//
CGFloat currentIncrement = MAX(pointsPhotoIsMovedBy.x, pointsPhotoIsMovedBy.y);
for (NSIndexPath *indexPath in self.galleryCollectionView.indexPathsForVisibleItems) {
UICollectionViewCell *cell = [self.galleryCollectionView cellForItemAtIndexPath:indexPath];
if ([indexPath isEqual:[NSIndexPath indexPathForItem:0 inSection:0]]) {
//
// Front.
//
CGFloat frontScalePercent = MIN(GalleryCollectionViewLayoutFrontRatioDefault,
MAX(GalleryViewFrontScaleMinimum, GalleryCollectionViewLayoutFrontRatioDefault - (currentIncrement / 250.0f)));
[cell setTransform:CGAffineTransformMakeScale(frontScalePercent, frontScalePercent)];
} else if ([indexPath isEqual:[NSIndexPath indexPathForItem:1 inSection:0]]) {
//
// Next.
//
CGFloat nextScalePercent = MAX(GalleryCollectionViewLayoutBackRatioDefault,
MIN(GalleryViewNextScaleMaximum, (currentIncrement / 150.0f)));
[cell setTransform:CGAffineTransformMakeScale(nextScalePercent, nextScalePercent)];
} else {
//
// Background.
//
[cell setTransform:CGAffineTransformMakeScale(GalleryCollectionViewLayoutBackRatioDefault,
GalleryCollectionViewLayoutBackRatioDefault)];
}
}
} else if (gesture.state == UIGestureRecognizerStateEnded) {
//
// Ended.
//
if (gesture.view.transform.a == GalleryViewFrontScaleMinimum) {
//
// Next photo.
//
[self.galleryCollectionView setUserInteractionEnabled:NO];
[self.galleryCollectionView sendSubviewToBack:gesture.view];
[UIView animateWithDuration:0.3f
animations:^{
[gesture.view setCenter:self.galleryCollectionView.center];
[gesture.view setTransform:CGAffineTransformMakeScale(GalleryCollectionViewLayoutBackRatioDefault,
GalleryCollectionViewLayoutBackRatioDefault)];
}
completion:^(BOOL finished) {
//
// Data source
//
NSMutableArray *photos = [self.photos mutableCopy];
[photos addObject:[photos objectAtIndex:0]];
[photos removeObjectAtIndex:0];
[self setPhotos:photos];
// Contents
//
[self.galleryCollectionView performBatchUpdates:^{
[self.galleryCollectionView deleteItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:0 inSection:0]]];
[self.galleryCollectionView insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:(self.photos.count - 1) inSection:0]]];
} completion:^(BOOL finished) {
[self.galleryCollectionView reloadData];
[self.galleryCollectionView setUserInteractionEnabled:YES];
}];
}];
} else {
//
// Stay.
//
[UIView animateWithDuration:0.3f
delay:0.0f
options:UIViewAnimationOptionCurveEaseIn
animations:^{
//
// Front cell.
//
[gesture.view setCenter:self.galleryCollectionView.center];
[gesture.view setTransform:CGAffineTransformMakeScale(GalleryCollectionViewLayoutFrontRatioDefault,
GalleryCollectionViewLayoutFrontRatioDefault)];
// Next cell.
//
for (NSIndexPath *indexPath in self.galleryCollectionView.indexPathsForVisibleItems) {
if ([indexPath isEqual:[NSIndexPath indexPathForItem:1 inSection:0]]) {
//
// Next.
//
UICollectionViewCell *cell = [self.galleryCollectionView cellForItemAtIndexPath:indexPath];
[cell setTransform:CGAffineTransformMakeScale(GalleryCollectionViewLayoutBackRatioDefault,
GalleryCollectionViewLayoutBackRatioDefault)];
}
}
}
completion:^(BOOL finished) {
[galleryCollectionViewLayout setFrontRatio:GalleryCollectionViewLayoutFrontRatioDefault];
[galleryCollectionViewLayout setNextRatio:GalleryCollectionViewLayoutBackRatioDefault];
[galleryCollectionViewLayout invalidateLayout];
}];
}
}
And here is my prepareLayout: code for the collection view layout:
NSMutableDictionary *newLayoutInfo = [NSMutableDictionary dictionary];
NSMutableDictionary *cellLayoutInfo = [NSMutableDictionary dictionary];
NSIndexPath *indexPath;
for (NSInteger section = 0; section < [self.collectionView numberOfSections]; section++) {
NSInteger itemCount = [self.collectionView numberOfItemsInSection:section];
for (NSInteger item = 0; item < itemCount; item++) {
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
[itemAttributes setSize:[self sizeForPhotoAtIndexPath:indexPath inContainerOfSize:self.collectionView.bounds.size]];
[itemAttributes setCenter:self.collectionView.center];
// Z Index.
//
NSInteger zIndex = itemCount - item;
[itemAttributes setZIndex:zIndex];
// Scale cells based on z position.
//
if (zIndex == itemCount) {
//
// Foreground.
//
[itemAttributes setTransform:CGAffineTransformMakeScale(self.frontRatio, self.frontRatio)];
} else if (zIndex == itemCount - 1) {
//
// Next.
//
[itemAttributes setTransform:CGAffineTransformMakeScale(self.nextRatio, self.nextRatio)];
} else {
//
// Background.
//
[itemAttributes setTransform:CGAffineTransformMakeScale(GalleryCollectionViewLayoutBackRatioDefault,
GalleryCollectionViewLayoutBackRatioDefault)];
}
cellLayoutInfo[indexPath] = itemAttributes;
}
}
newLayoutInfo[GalleryCollectionViewLayoutCellKind] = cellLayoutInfo;
self.layoutInfo = newLayoutInfo;
And as an additional reference, here is my code for sizeForPhotoAtIndexPath: inContainerOfSize:
UIImage *photo = [[(DIGalleryViewController *)self.collectionView.delegate photos] objectAtIndex:indexPath.row];
CGSize size = CGSizeMake(photo.size.width, photo.size.height);
UIGraphicsBeginImageContext(containerSize);
[photo drawInRect:CGRectMake(0.0f, 0.0f, containerSize.width, containerSize.height)];
UIImage *resizedPhoto = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGFloat ratio = photo.size.width / resizedPhoto.size.width;
size = CGSizeMake(resizedPhoto.size.width, photo.size.height / ratio);
return size;