Remove Oscillation from UIAttachmentBehavior in UICollectionView - uicollectionview

I am attempting to recreate the spring behavior that you see in the iOS Messages app in my UICollectionView. Like Messages it will have various cell sizes based on the text size. I have created a custom UICollectionViewFlowLayout which does add the behavior to the UICollectionView however the message bubbles continue to oscillate slightly after the user has stopped scrolling. I have tried any number of combinations in the length, damping and spring values but the oscillation never goes away.
After some reading of other stack questions I did find this comment
In order to prevent oscillation it's necessary to dynamically increase the damping factor on a quadratic scale as the attached views get closer and closer to their attachment points. <
But I am not really sure where to get started with implementing something like that on what I currently have. Any help or guidance would be appreciated.
Below is my code on the UICollectionViewFlowLayout that is creating the current effect.
- (void) prepareLayout {
[super prepareLayout];
CGRect originalRect = (CGRect){.origin = self.collectionView.bounds.origin, .size = self.collectionView.frame.size};
CGRect visibleRect = CGRectInset(originalRect, -50, -50);
NSArray *itemsInVisibleRectArray = [super layoutAttributesForElementsInRect:visibleRect];
NSSet *itemsIndexPathsInVisibleRectSet = [NSSet setWithArray:[itemsInVisibleRectArray valueForKey:#"indexPath"]];
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UIAttachmentBehavior *behaviour, NSDictionary *bindings) {
BOOL currentlyVisible = [itemsIndexPathsInVisibleRectSet member:[[[behaviour items] firstObject] indexPath]] != nil;
return !currentlyVisible;
}];
NSArray *noLongerVisibleBehaviours = [self.animator.behaviors filteredArrayUsingPredicate:predicate];
[noLongerVisibleBehaviours enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
[self.animator removeBehavior:obj];
[self.visibleIndexPathsSet removeObject:[[[obj items] firstObject] indexPath]];
}];
NSPredicate *newPredicate = [NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes *item, NSDictionary *bindings) {
BOOL currentlyVisible = [self.visibleIndexPathsSet member:item.indexPath] != nil;
return !currentlyVisible;
}];
NSArray *newlyVisibleItems = [itemsInVisibleRectArray filteredArrayUsingPredicate:newPredicate];
CGPoint touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];
[newlyVisibleItems enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *item, NSUInteger idx, BOOL *stop) {
CGPoint center = item.center;
UIAttachmentBehavior *springBehaviour = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:center];
springBehaviour.length = 0.1f;
springBehaviour.damping = 3.0f;
springBehaviour.frequency = 2.8f;
if (!CGPointEqualToPoint(CGPointZero, touchLocation)) {
CGFloat yDistanceFromTouch = fabs(touchLocation.y - springBehaviour.anchorPoint.y);
CGFloat xDistanceFromTouch = fabs(touchLocation.x - springBehaviour.anchorPoint.x);
CGFloat scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0f;
if (self.latestDelta < 0) {
center.y += MAX(self.latestDelta, self.latestDelta*scrollResistance);
}
else {
center.y += MIN(self.latestDelta, self.latestDelta*scrollResistance);
}
item.center = center;
}
[self.animator addBehavior:springBehaviour];
[self.visibleIndexPathsSet addObject:item.indexPath];
}];
}

You can fix it with 2 steps.
1. add action for the behavior when initials to make sure the center of the cell doesn't change during the animation
springBehaviour.action = ^{
CGPoint itemCenter = item.center;
itemCenter.x = center.x;
item.center = itemCenter;
};
remove/re-add the behaviors when collectionview stops scrolling. To do this, you need implement a scrollview delegate method and in this method to remove/re-add behaviors.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
ZZHCollectionFlowLayout *flowLayout = self.collectionView.collectionViewLayout;
if ([flowLayout isKindOfClass:[ZZHCollectionFlowLayout class]])
{
[flowLayout removeAnimationBehavior];
}
else
{
// Your NSAssertionHandler
}
}
- (void)removeAnimationBehavior
{
NSArray *behaviors = self.dynamicAnimator.behaviors;
[self.dynamicAnimator removeAllBehaviors];
for (UIDynamicBehavior *obj in behaviors)
{
[self.dynamicAnimator addBehavior:obj];
}
}
BTW, if there is way of changing damping to fix, would like to hear it!

Related

Image in UITableView using too much memory

I created a class for download images from URLs for UITableViewCells (in this project I cannot use SDWebImageView or other codes from internet) but it looks like it's using a lot of memory and my tableview is not loading so fast. Can anybody point what is the problem?
Code:
//MyHelper class
+(NSString *)pathForImage:(NSString *)urlImageString{
if ([urlImageString class] == [NSNull class] || [urlImageString isEqualToString:#"<null>"] || [urlImageString isEqualToString:#""]) {
return #"";
}
NSArray *pathsInString = [urlImageString componentsSeparatedByString:#"/"];
NSString *eventCodeString = [pathsInString objectAtIndex:[pathsInString count] - 2];
NSString *imageNameString = [pathsInString lastObject];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachePath = [paths objectAtIndex:0];
cachePath = [MyHelper validateString:[cachePath stringByAppendingString:eventCodeString]];
[cachePath stringByAppendingString:#"/"];
return [cachePath stringByAppendingString:imageNameString];
}
+(BOOL)imageExistsForURL:(NSString *)urlString{
if (!([urlString class] == [NSNull class]))
{
NSString *filePath = [MyHelper pathForImage:urlString];
NSFileManager *fileManager = [NSFileManager defaultManager];
return [fileManager fileExistsAtPath:filePath];
}
return false;
}
+(void)setAsyncImage:(UIImageView *)imageView forDownloadImage:(NSString *)urlString{
CGRect activityFrame = CGRectMake(0, 0, 60, 60);
UIActivityIndicatorView *activity = [[UIActivityIndicatorView alloc] initWithFrame:activityFrame];
activity.layer.cornerRadius = activity.frame.size.width / 2;
activity.clipsToBounds = YES;
activity.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
[imageView addSubview:activity];
[activity startAnimating];
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
NSData *image;
if ([urlString class] == [NSNull class]) {
image = nil;
} else {
image = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:urlString]];
}
dispatch_async(dispatch_get_main_queue(), ^{
[activity stopAnimating];
[activity removeFromSuperview];
if (image)
{
[UIView animateWithDuration:0.9 animations:^{
imageView.alpha = 0;
imageView.image = [UIImage imageWithData:image];
imageView.alpha = 1;
}];
NSString *filePath = [MyHelper pathForImage:urlString];
NSError *error;
[image writeToFile:filePath options:NSDataWritingAtomic error:&error];
}
else
{
imageView.image = [UIImage imageNamed:#"icn_male.png"];
}
});
});
}
+(NSString *)validateString:(NSString *)string{
if (string == (id)[NSNull null] || string.length == 0 )
return #"";
return string;
}
+ (UIImage*)imageWithImage:(UIImage*)image
scaledToSize:(CGSize)newSize;
{
float proportion;
if (image.size.height > image.size.width) {
proportion = image.size.height / newSize.height;
} else {
proportion = image.size.width / newSize.width;
}
UIGraphicsBeginImageContext( newSize );
[image drawInRect:CGRectMake(newSize.width - (image.size.width/proportion),
newSize.height/2 - (image.size.height/proportion)/2,
image.size.width/proportion,
image.size.height/proportion)];
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
Using this code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellIdentifier = #"MyCell";
MyCell *cell = (MyCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
if ([MyHelper imageExistsForURL:photoURLString ]) {
UIImage *image = [UIImage imageWithContentsOfFile:[MyHelper pathForImage:photoURLString]];
eventImageView.image = [MyHelper imageWithImage:image scaledToSize:CGSizeMake(60, 60)];
} else {
[MyHelper setAsyncImage:eventImageView forDownloadImage:photoURLString ];
}
}
Since it is now clear that you are using oversized images, the solution is to figure out how big your images need to be in order to look good in your app.
There are several courses of action depending on how much you can change the server side portion of your system.
Use an image that is optimally sized for the highest-res case (3x) and let 2x and 1x devices do the scaling. This is again a bit wasteful.
Create some scheme whereby you will be able to get the right size image for your device type (perhaps by appending 2x, 3x etc.) to the image file name. Arguably the best choice.
Do the resizing on the client side. This can be somewhat CPU intensive and is probably the worst approach in my opinion because you will be doing a lot of work unnecessarily. However, if you can't change how your server works, then this is your only option.
Another problem with your code is that you are doing the resizing on the main/UI thread, which is blocking your UI, which is a no-no. Never perform long operations on the main thread.
You should be doing it on a background thread using dispatch_async or perhaps NSOperation and a sequential queue to reduce memory usage. Note that this can create new problems because you have to update your image view when the image is ready and consider things such as whether the cell is still visible or not. I came across a nice blog post on this a while back so I suggest searching the web.
However, if the images are really huge, then maybe you could consider setting up a proxy server and then getting resized images from there instead of the main server. Of course, you would have to consider intellectual property issues in this case.

SpriteKit enumerateChildNodesWithName with advanced searching?

The following code doesn't work as expected. According to the SPRITE KIT PROGRAMMING GUIDE, pages 61 and 62 one may perform "advanced searches" by using regular expression like syntax, so, unless I'm misunderstanding the implementation, this should work?
SKShapeNode *myCircle = [self getCircle];
myCircle.name = [NSString stringWithFormat:#"CIRCLE_%d_%d", x, y];
myCircle.position = CGPointMake(10,10);
[self addChild:myCircle];
// Lets remove ALL SKNodes that begin with the name "CIRCLE_"
[self enumerateChildNodesWithName:#"CIRCLE_*" usingBlock:^(SKNode *node, BOOL *stop) {
[node removeFromParent];
}];
But alas, the nodes do not go away. If I specify an exact name (like #"CIRCLE_10_10") it works, but the wildcard expression * doesn't seem to, nor does something like this #"CIRCLE_[0-9]+_[0-9]+" -- not even if I use slashes #"/CIRCLE_[0-9]+_[0-9]+".
What am I doing wrong?
EDIT:
THIS WORKS and I could implement regular expression matching instead of substring'ing, but hoping to get the Sprite Kit implementation working (ideally).
[[self children] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
SKNode *node = (SKNode *)obj;
if ([[node.name substringWithRange:NSMakeRange(0, 7)] isEqual: #"CIRCLE_"]) {
[node removeFromParent];
}
}];
I tried the following variation of your code in both the current and beta versions of Xcode on both iOS and OS-X.
SKScene *scene = [[SKScene alloc] initWithSize:self.gameView.frame.size];
for (int x = 0; x < 20; x++)
{
for (int y = 0; y < 20; y++)
{
CGPathRef myPath = CGPathCreateWithEllipseInRect(CGRectMake(0, 0, 20, 20), NULL);
SKShapeNode *myCircle = [[SKShapeNode alloc] init];
myCircle.path = myPath;
#if TARGET_OS_IPHONE
myCircle.fillColor = [UIColor redColor];
#else
myCircle.fillColor = [NSColor redColor];
#endif
myCircle.name = [NSString stringWithFormat:#"CIRCLE_%d_%d", x, y];
myCircle.position = CGPointMake(20*x,20*y);
[scene addChild:myCircle];
CGPathRelease(myPath);
}
}
[self.gameView presentScene:scene];
All of your sample expressions work on the Mac in both versions of the SDK. On iOS, however, they only worked in the Developer Preview.

custom Annotations being switched when reloaded on MKMapView

I've been having this issue for a couple of weeks now, and I still have not found an answer. on my MapView I have custom annotations, and when I hit the "reload button" all the information is correct as in the annotation "title, subtitle". but the annotation has changed. the annotations are in a NSMutableArray and I'm sure that the issue i am having revolves around that. here is the code I am using to reload the annotations.
so just prevent any confusion, my custom annotations work just fine when i first load the mapView. But once i hit the reload button, all the annotation's information like "location,title, subtitle" all that is correct, just the actual annotation has changed. Like all the annotations have been switched around.
if anyone can help, it would greatly be appreciated! thanks!
- (IBAction)refreshMap:(id)sender {
NSArray *annotationsOnMap = myMapView.annotations;
[myMapView removeAnnotations:annotationsOnMap];
[locations removeAllObjects];
[citiesArray removeAllObjects];
[self retrieveData];
}
-(void) retrieveData {
userLAT = [NSString stringWithFormat:#"%f", myMapView.userLocation.coordinate.latitude];
userLNG = [NSString stringWithFormat:#"%f", myMapView.userLocation.coordinate.longitude];
NSString *fullPath = [mainUrl stringByAppendingFormat:#"map_json.php?userID=%#&lat=%#&lng=%#",theUserID,userLAT,userLNG];
NSURL * url =[NSURL URLWithString:fullPath];
NSData *data = [NSData dataWithContentsOfURL:url];
json =[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
citiesArray =[[NSMutableArray alloc]init];
for (int i = 0; i < json.count; i++)
{
//create city object
NSString * eID =[[json objectAtIndex:i] objectForKey:#"userid"];
NSString * eAddress =[[json objectAtIndex:i] objectForKey:#"full_address"];
NSString * eHost =[[json objectAtIndex:i] objectForKey:#"username"];
NSString * eLat =[[json objectAtIndex:i] objectForKey:#"lat"];
NSString * eLong =[[json objectAtIndex:i] objectForKey:#"lng"];
NSString * eName =[[json objectAtIndex:i] objectForKey:#"Restaurant_name"];
NSString * eState = [[json objectAtIndex:i] objectForKey:#"type"];
NSString * annotationPic = [[json objectAtIndex:i] objectForKey:#"Annotation"];
NSString * eventID = [[json objectAtIndex:i] objectForKey:#"id"];
//convert lat and long from strings
float floatLat = [eLat floatValue];
float floatLONG = [eLong floatValue];
City * myCity =[[City alloc] initWithRestaurantID: (NSString *) eID andRestaurantName: (NSString *) eName andRestaurantState: (NSString *) eState andRestaurantAddress: (NSString *) eAddress andRestaurantHost: eHost andRestaurantLat: (NSString *) eLat andRestaurantLong: (NSString *) eLong];
//Add our city object to our cities array
// Do any additional setup after loading the view.
[citiesArray addObject:myCity];
//Annotation
locations =[[NSMutableArray alloc]init];
CLLocationCoordinate2D location;
Annotation * myAnn;
//event1 annotation
myAnn =[[Annotation alloc]init];
location.latitude = floatLat;
location.longitude = floatLONG;
myAnn.coordinate = location;
myAnn.title = eName;
myAnn.subtitle = eHost;
myAnn.type = eState;
myAnn.AnnotationPicture = annotationPic;
myAnn.passEventID = eventID;
myAnn.hotZoneLevel = hotZone;
[locations addObject:myAnn];
[self.myMapView addAnnotations:locations];
}
}
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
if([annotation isKindOfClass:[MKUserLocation class]])
return nil;
static NSString *annotationIdentifier = #"AnnotationIdentifier";
MKAnnotationView *annotationView = (MKAnnotationView *) [self.myMapView
dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier];
if (!annotationView)
{
annotationView = [[MKAnnotationView alloc]
initWithAnnotation:annotation
reuseIdentifier:annotationIdentifier];
NSString *restaurant_Icon = ((Annotation *)annotation).AnnotationPicture;
NSString *restaurant_Callout = [NSString stringWithFormat:#"mini.%#",restaurant_Icon];
UIImage *oldImage = [UIImage imageNamed:restaurant_Icon];
UIImage *newImage;
CGSize newSize = CGSizeMake(75, 75);
newImage = [oldImage imageScaledToFitSize:newSize]; // uses MGImageResizeScale
annotationView.image= newImage;
annotationView.canShowCallout = YES;
UIImage *Mini_oldImage = [UIImage imageNamed:event_Callout];
UIImage *Mini_newImage;
CGSize Mini_newSize = CGSizeMake(30,30);
Mini_newImage = [Mini_oldImage imageScaledToFitSize:Mini_newSize]; // uses MGImageResizeScale
UIImageView *finalMini_callOut = [[UIImageView alloc] initWithImage:Mini_newImage];
annotationView.leftCalloutAccessoryView = finalMini_callOut;
UIButton* rightButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
annotationView.rightCalloutAccessoryView = rightButton;
}
else
{
annotationView.annotation = annotation;
}
return annotationView;
}
If nothing else, you're setting the icon and the callout based upon the annotation, but only doing that in viewForAnnotation if the annotation was not dequeued. You really want to do any annotation-specific customization outside of that if block, in case an annotation view is reused.
Unrelated to your reported issue, there are a few other observations:
You probably should be doing retrieveData asynchronously so you don't tie up the main thread with your data retrieval/parsing. Go ahead and dispatch the adding of the entry to your array and adding the annotation to the map in the main queue, but the network stuff and should definitely be done asynchronously.
You probably should check to make sure data is not nil (e.g. no network connection or some other network error) because JSONObjectWithData will crash if you pass it a nil value.
Your use of locations seems unnecessary because you're resetting it for every entry in your JSON. You could either (a) retire locations entirely and just add the myAnn object to your map's annotations; or (b) initialize locations before the for loop. But it's probably misleading to maintain this ivar, but only populate it with the last annotation.

Reversing sprite animation

After making adjustments to my code as suggested, my animation now works. Now, I'd like to ask how to reverse the movements of the sprite. I am trying to make the sprite move as if the wind is blowing in it. Here's my code now:
-(id) init
{
// always call "super" init
// Apple recommends to re-assign "self" with the "super's" return value
if( (self=[super init]) ) {
CGSize winSize = [[CCDirector sharedDirector] winSize];
self.isTouchEnabled = YES;
CCSprite *black = [CCSprite spriteWithFile:#"b.png"];
black.position = ccp(100, 160);
black.scaleX = 100 / black.contentSize.width;
black.anchorPoint = ccp(0.03, 0);
CCSpriteFrameCache *frame = [CCSpriteFrameCache sharedSpriteFrameCache];
[frame addSpriteFramesWithFile:#"bLongAnimation.plist"];
CCSpriteBatchNode *bHair = [CCSpriteBatchNode batchNodeWithFile:#"bLongAnimation.png"];
[self addChild:bHair];
[self addChild:black z:1 tag:1];
//Animation
NSMutableArray *animateBlackHair = [NSMutableArray arrayWithCapacity:10];
for (int i = 1; i < 10; i++)
{
NSString *animBlackHair = [NSString stringWithFormat:#"b%i.png", i];
CCSpriteFrame *blackFrame = [frame spriteFrameByName:animBlackHair];
[animateBlackHair addObject:blackFrame];
//I added this code block thinking it might work
for(i = 10; i > 1; i--)
{
NSString *revAnimation = [NSString stringWithFormat:#"bRightLong%i.png", i];
CCSpriteFrame *revFrame = [frame spriteFrameByName:revAnimation];
[animateBlackHair1 addObject:revFrame];
}
}
CCAnimation *blowHair = [CCAnimation animationWithSpriteFrames:animateBlackHair delay:0.1];
CCAction *blowingHair = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:blowHair]];
[black runAction:blowingHair];
}
return self;}
My guess didn't work so I was wondering how I can reverse the movements as soon as it finishes the first one?
UPDATE: Never mind, I figured it out. I just moved the for loop for reversing the action outside of the other loop. Thank you for the help.
Try setting the delay on the animation to something other than 0. Try 0.1 or 0.4 or something. If you set it to zero I don't think the animation runs, and if it does, it runs too fast to be visible.

Create an array of UIImages from camera roll

I would like to get all of the images from the camera roll and create an array of UIImages from them.
I have been trying to figure out how to do this for about a day now and I've gotten nowhere. I can't seem to figure out how to retrieve only items from the Camera Roll. It appears that all of the samples that I've seen all enumerate over all of the photo albums. I might be wrong about that though.
Any help would be appreciated. Thanks!
Have you tried ALAssetsLibrary? like this:
assets = [[NSMutableArray array] init]; // Prepare array to have retrieved images by Assets Library.
void (^assetEnumerator)(ALAsset *, NSUInteger, BOOL *) = ^(ALAsset *asset, NSUInteger index, BOOL *stop) {
if(asset != NULL) {
[assets addObject:asset];
dispatch_async(dispatch_get_main_queue(), ^{
[self insertArray];
});
}
};
void (^assetGroupEnumerator)(ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop) {
if(group != nil) {
[group enumerateAssetsUsingBlock:assetEnumerator];
}
};
// Create instance of the Assets Library.
library = [[ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos // Retrieve the images saved in the Camera roll.
usingBlock:assetGroupEnumerator
failureBlock: ^(NSError *error) {
NSLog(#"Failed.");
}];
that'll nab them. then do this to render them (this is wicked hacky. you'll want to import them as needed and not all at once like this, or you'll run into memory issues and crash)
-(void) insertArray {
int i = assetCount++;
if (i>20) {
return;
}
ALAssetRepresentation *rep = [[assets objectAtIndex:i] defaultRepresentation];
CGImageRef iref = [rep fullResolutionImage];
CGSize cSize = CGSizeMake(75,75);
UIImage *largeimage = [UIImage imageWithCGImage:iref];
UIImage *resizedimage = (UIImage *)[largeimage resizedImage:cSize interpolationQuality:kCGInterpolationHigh];
UIImageView *newView = [[UIImageView alloc] initWithImage:resizedimage];
if((i>0)&&(i%4 == 0)){
rowCount++;
}
colCount = i%4;
newView.frame = CGRectMake(4+(colCount*(75+4)), 4+(rowCount*(75+4)), 75, 75);
[sv addSubview:newView];
[sv setContentSize:CGSizeMake(320, 85+(rowCount*(75+4)))];
NSLog(#"sv frame size is %# and i is %i", NSStringFromCGRect(sv.frame), i);
}