iOS building custom rating control issue - ios

I know there's lots of 3rd party solutions out there, but I've decided to build one myself to be in full control of the animations and to learn something new.
It seems like most of the time it is made through subclassing UIControl and tracking touches, but I've used a different approach. I create system buttons with images. This way I get a nice highlight animation on a press for free. Anyway, it works quite nice until you press on it really fast. Here's a gif of what's happening.
Sometimes, if you do it really fast a full star gets stuck. I'll try to explain what's happening behind this animation. Since full and empty stars have different color, I change tint color of the button each time i becomes full or empty. The animation you see is a separate UIImageView that is added on top of the button right before the beginning of the animation block and is removed in the completion block. I believe sometimes on emptying the star animation is not fired and image is not removed in the completion block. But I can't catch this bug in code.
I added some debugging code to see if the block that makes a star empty is even fired. Seems like it is, and even completion block is called.
2015-04-23 13:00:00.416 RERatingControl[24011:787202]
Touch number: 5
2015-04-23 13:00:00.416 RERatingControl[24011:787202]
removing a star at index: 4
2015-04-23 13:00:00.693 RERatingControl[24011:787202] star removed
Star indexes are zero-based, so the 5th star is supposed to be removed, but it's not.
Full project is on github. It's pretty small. I would appreciate if someone would help me fix this. Oh, and feel free to use this code if you want to :-)
UPDATE. I was suggested to post some logic code here. I didn't do it right away because I felt it would be too cumbersome.
Button action method:
- (void)buttonPressed:(id)sender {
static int touchNumber = 0;
NSLog(#"\nTouch number: %d", touchNumber);
touchNumber++;
if (self.isUpdating) {
return;
}
self.updating = YES;
NSInteger index = [self.stars indexOfObject:sender];
__block NSTimeInterval delay = 0;
[self.stars enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
REStarButton *btn = (REStarButton*)obj;
if (idx <= index) {
if (!btn.isFull) {
NSLog(#"\nadding a star at index: %ld", (long)idx);
[btn makeFullWithDelay:delay];
delay += animationDelay;
}
} else {
if (btn.isFull) {
NSLog(#"\nremoving a star at index: %ld", (long)idx);
[btn makeEmptyWithDelay:0];
}
}
}];
if ([self.delegate respondsToSelector:#selector(ratingDidChangeTo:)]) {
[self.delegate ratingDidChangeTo:index + 1];
}
self.updating = NO;
}
Empty a full star method:
- (void)makeEmptyWithDelay:(NSTimeInterval)delay {
// if (!self.isFull) {
// return;
// }
self.full = NO;
UIImageView *fullStar = [[UIImageView alloc] initWithImage:self.starImageFull];
[self addSubview:fullStar];
CGFloat xOffset = CGRectGetMidX(self.bounds) - CGRectGetMidX(fullStar.bounds);
CGFloat yOffset = CGRectGetMidY(self.bounds) - CGRectGetMidY(fullStar.bounds);
fullStar.frame = CGRectOffset(fullStar.frame, xOffset, yOffset);
[self setImage:self.starImageEmpty forState:UIControlStateNormal];
[self setTintColor:self.superview.tintColor];
[UIView animateWithDuration:animationDuration delay:delay options:UIViewAnimationOptionCurveEaseOut animations:^{
//fullStar.transform = CGAffineTransformMakeScale(0.1f, 0.1f);
fullStar.transform = CGAffineTransformMakeTranslation(0, 10);
fullStar.alpha = 0;
} completion:^(BOOL finished) {
[fullStar removeFromSuperview];
NSLog(#"star removed");
}];
Make an empty star full method:
}
- (void)makeFullWithDelay:(NSTimeInterval)delay {
// if (self.isFull) {
// return;
// }
self.full = YES;
UIImageView *fullStar = [[UIImageView alloc] initWithImage:self.starImageFull];
CGFloat xOffset = CGRectGetMidX(self.bounds) - CGRectGetMidX(fullStar.bounds);
CGFloat yOffset = CGRectGetMidY(self.bounds) - CGRectGetMidY(fullStar.bounds);
fullStar.frame = CGRectOffset(fullStar.frame, xOffset, yOffset);
fullStar.transform = CGAffineTransformMakeScale(0.01f, 0.01f);
[self addSubview:fullStar];
[UIView animateKeyframesWithDuration:animationDuration delay:delay options:0 animations:^{
CGFloat ratio = 0.35;
[UIView addKeyframeWithRelativeStartTime:0 relativeDuration:ratio animations:^{
fullStar.transform = CGAffineTransformMakeScale(animationScaleRatio, animationScaleRatio);
}];
[UIView addKeyframeWithRelativeStartTime:ratio relativeDuration:1 - ratio animations:^{
fullStar.transform = CGAffineTransformIdentity;
}];
} completion:^(BOOL finished) {
[self setImage:self.starImageFull forState:UIControlStateNormal];
[self setTintColor:self.fullTintColor];
[fullStar removeFromSuperview];
}];
}

Ah, I've managed to find a reason behind my bug and it works perfect now!
I used View Debugging to see if it's an UIImageView from animation is stuck on the button or the image of button itself is not set properly. I was surprised to see it's the button image that causing the issue. And then I looked through code again and realised I was changing the button image in the completion block after delay with no regard to if it's state has already changed. A simple fix to makeFullWithDelay: solved my issue.
} completion:^(BOOL finished) {
if (self.isFull) {
[self setImage:self.starImageFull forState:UIControlStateNormal];
[self setTintColor:self.fullTintColor];
}
[fullStar removeFromSuperview];
}];
I used whole evening trying to find the answer yesterday, and after I posted the question answer came by itself. Telling people about your problem really helps to go through your logic with more attention I guess. Thanks to everyone :-)
Should I delete the post or it might help someone in the future?

Related

didEnterRegion and didExitRegion animation conflicting

I'm building an app that uses GeoFencing. My didEnterRegion and didExitRegion methods are called as they're supposed to, seemingly in the correct order, but fails to update the UI accordingly.
What works:
User enters region
didEnterRegion gets called
UI is updated correctly within that method
What doesn't work:
User enter region
User goes straight from one region to another
didExitRegion is called
UI updates
didEnterRegion is called
NSLogs indicate that everything executes in the correct order
UI isn't updated. The UI updates that were done in didExitRegion remains.
My methods:
Custom function to update label (called from didEnterRegion and didExitRegion):
-(void)updateCurrentLocationLabelAndImage:(NSString *)locationText subLocationText:(NSString *)subLocationText;
{
// Clear existing animations before we begin
[self.locationLabel.layer removeAllAnimations];
[self.subLocationLabel.layer removeAllAnimations];
[self.ovalImageView.layer removeAllAnimations];
if (![locationText isEqual:#""])
{
// Only animate if the text changes
if (![self.locationLabel.text isEqualToString:locationText])
{
// Update the ovalImageView
CGSize maxLabelSize = CGSizeMake(([UIScreen mainScreen].bounds.size.width - (([UIScreen mainScreen].bounds.size.width * 0.0267) * 2)), 64); // maximum label size
float expectedLabelWidth = [locationText boundingRectWithSize:maxLabelSize options:NSStringDrawingUsesLineFragmentOrigin attributes:#{ NSFontAttributeName:self.locationLabel.font } context:nil].size.width; // get expected width
CGFloat xValueForImage = ((([UIScreen mainScreen].bounds.size.width - expectedLabelWidth) / 2) - 25); // Calcuate the x-coordinate for the ovalImageView
if (xValueForImage < 15)
{
xValueForImage = 15; // we don't want it to display off-screen
}
self.ovalImageViewLeadingConstraint.constant = xValueForImage;
[self.view setNeedsUpdateConstraints];
[self changeLabelText:self.subLocationLabel string:subLocationText];
// Update the subLocationLabel
[UIView animateWithDuration:.15 animations:^{
// Animate
self.locationLabel.alpha = 0;
self.ovalImageView.alpha = 1;
[self.view layoutIfNeeded]; // update the UI
}completion:^(BOOL finished) {
// Set the text
self.locationLabel.text = locationText;
self.locationLabel.adjustsFontSizeToFitWidth = YES;
[UIView animateWithDuration:.15 animations:^{
// Animate
self.locationLabel.alpha = 1;
}completion:^(BOOL finished) {
// Complete
}];
}];
}
} else if ([locationText isEqual:#""])
{
// Move it to the center
self.ovalImageViewLeadingConstraint.constant = (([UIScreen mainScreen].bounds.size.width / 2) - 9); // Default center calculation for this image
[self.view setNeedsUpdateConstraints];
[self changeLabelText:self.subLocationLabel string:subLocationText]; // Update the subLocationLabel
[UIView animateWithDuration:.15 animations:^{
self.locationLabel.alpha = 0;
self.ovalImageView.alpha = 1;
[self.view layoutIfNeeded];
}completion:^(BOOL finished) {
// Complete
self.locationLabel.text = #"";
}];
}
}
But the label stays the same, even though it logs the correct order of execution and everything looks fine. Any ideas? I'm really stuck..
Thanks!
To explain in detail,
lets consider a scenario of user exiting region1 and straight entering region2.
Step 1:
So, the event exit is fired, the following lines are executed,
NSLog(#"changing text from: %#, to: %#", label.text, string);
// Only animate if the text changes
if (![label.text isEqualToString:string])
At this If check, the label text is "inside region" (which is for region 1). Since the method is called from the exit method, the parameter string will have the value "Outside region". Both the values are not same and hence the control enters this region.
Step 2:
At this point you started an animation of duration 0.15, however the value of the label is not changed until the animation is completed as the code is in the completion block.
// Complete
label.text = string;
Step 3:
The did enter region 2 event is fired. The label text is not yet up to date, so it excites the below line
NSLog(#"changing text from: %#, to: %#", label.text, string);
However since the label text is "inside region" and string value is also "inside region", the control moves out of the if condition.
if (![label.text isEqualToString:string])
To verify:
Pass in a customized string value while calling change label text making the text specific viz., "inside region 1", "exiting region 1", "inside region 2" etc., This way the value will be different and would get updated.
Set the value of the label immediately before animation.
Assign the value to the atomic string property and check for the property Value.
for eg:
#property (atomic, strong) NSString * currentLabelText;
-(void)changeLabelText:(UILabel *)label string:(NSString *)string
{
NSLog(#"changing text from: %#, to: %#", label.text, string);
// Only animate if the text changes
if (![self. currentLabelText isEqualToString:string])
{
self. currentLabelText = string; //Ultimately this would be our value
[UIView animateWithDuration:.15 animations:^{
// Animate
label.alpha = 0;
}completion:^(BOOL finished) {
// Complete
label.text = self. currentLabelText;
[UIView animateWithDuration:.15 animations:^{
// Animate
label.alpha = 1;
}completion:^(BOOL finished) {
// Complete
}];
}];
}
}
Or another way as it suites your requirement.
You can try this…
static NSString *currentText;
currentText = locationText;
[UIView animateWithDuration:.15 animations: ^{
// Animate
self.locationLabel.alpha = 0;
self.ovalImageView.alpha = 1;
[self.view layoutIfNeeded]; // update the UI
} completion: ^(BOOL finished) {
// Set the text
if (![currentText isEqualToString:locationText]) {
return;
}
self.locationLabel.text = locationText;
self.locationLabel.adjustsFontSizeToFitWidth = YES;
[UIView animateWithDuration:.15 animations: ^{
// Animate
self.locationLabel.alpha = 1;
} completion: ^(BOOL finished) {
// Complete
}];
}];
I'd recommend you to put your UI update code in a dispatch block to make sure the UI-Update will performed on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
//... UI Update Code
});
I guess the main thread thing is your biggest problem. To me the code looks a bit strange at this points:
you are mixing different data types (float, CGFloat, int)
refer to CocoaTouch64BitGuide this could be a problem but I think it doesn't solve your layout issue. More critical are the magic numbers in general.
your autolayout code could be a problem
I believe it isn't necessary to move ovalImageView by manipulating the LeadingConstraint. Just set the contentMode to center/left/right and autolayout will do the reset. If you realy need to change the constraint then do it in [yourView updateConstraints] method. If you want the labels width ask for it: [label intrinsicContentsize] and use setPreferredMaxLayoutWidth if you need it. If your autolayout code is correct no view will appear "off-screen".
your animations could be a little bit jumpy
You use removeAllAnimations at first. Basically right but if you expect high frequented updates I'd recommend just to change the labels/images content while a previous animation runs. Otherwise your image jumps back and a new animation will start.

UIView Animation not working - iOS

I am trying to animate a simple UIImageView, but it is not working. Here is my code:
In my header file, I have declared "light_bulb" as a UIImageView.
NSMutableArray *bulb_off_images = [[NSMutableArray alloc] init];
for (int loop = 0; loop < 11; loop++) {
if (loop >= 10) {
[bulb_off_images addObject:[UIImage imageNamed:[NSString stringWithFormat:#"bulb_off-%d.png", loop]]];
}
else {
[bulb_off_images addObject:[UIImage imageNamed:[NSString stringWithFormat:#"bulb_off-0%d.png", loop]]];
}
}
[UIView animateWithDuration:0.5 delay:2.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
light_bulb.animationImages = bulb_off_images;
} completion:^(BOOL finished){
if (finished) {
NSLog(#"Finished !!!!!");
}
}];
The code seems to run and the completion block is called but the animation does not happen.....
**Update 1 **
I tried doing the animation this way, but I need a way of stopping the animation from repeating itself. How do I know when to call "stopAnimating". Here is my code:
light_bulb.animationImages = bulb_off_images;
light_bulb.animationDuration = 0.5;
light_bulb.animationRepeatCount = 0;
[light_bulb startAnimating];
What am I doing wrong?
In regards to your original question, you're over-complicating your solution. UIImageView has animation methods built-in. It looks like you're trying to use them in conjunction with UIView's animation features, which won't work exactly the way you're planning. Review the docs for each, and decide which classes' animation features you would like to use.
(For images, UIImageView's animation methods seem to be the preferred way to go.)
As for your second question: in the documentation for UIImageView, the notes for animationRepeatCount state:
The default value is 0, which specifies to repeat the animation indefinitely.
Setting your animationRepeatCount to 1 should play the animation only once.

Quick series of [UIView transitionWithView...] not going smoothly

I've got a view which is a 4x4 grid of images, and am trying to achieve the effect of every square, going from top to bottom and left to right, flipping over to reveal a new image. Should be a bit of a "ripple" effect, so the top left square starts flipping immediately, the [0,1] square flips after 0.05 seconds, [0,2] after 0.10 seconds, and so on, so that after (0.05*15) seconds the final square starts to flip.
My implementation is this: Each grid square is a UIView subclass, DMGridSquareView, which contains a UIImageView, and the following method:
- (void)flipToNewImage:(UIImage*)img {
AATileView *mySelf = self;
[UIView transitionWithView:self.imageView
duration:0.4
options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{
self.imageView.image = img;
} completion:^(BOOL finished) {
NSLog(#"Finished flip");
}];
}
Then, in order to trigger the animation of all the grid squares, within the DMGridView, I call this method:
- (void)flipAllSquares:(NSArray*)newImages {
int numTiles = self.numColumns * self.numRows;
for (int idx=0; idx<numTiles; idx++) {
UIImage *newImg = [newImages objectAtIndex:idx];
[[self.gridSquareViews objectAtIndex:idx] performSelector:#selector(flipToNewImage:) withObject:newImg afterDelay:(0.05*idx)];
}
}
When running on the simulator, this works beautifully. But when running on my iPhone 5, it has an unexpected (to me, at least) behavior. The animations of the individual grid squares are nice and smooth, and at the correct speed, but the start times are off. They happen in staggered "chunks". So the first 2 squares might flip immediately, then the next 4 squares will simultaneously flip after a moment, then the next 3 flip after another moment, and so on. It's not consistent, but it's always in chunks like this.
Any idea how to fix this?
Thanks.
EDIT 1:
Sorry, it looks like I misunderstood your question. You do not want them to flip one after another, you would like each view to begin flipping very soon after the previous view has begun flipping.
Have you tried calling the method recursively from the completion block? This way you know the method is only being called after the previous image has completed the flip animation.
NSInteger imageFlipIndex = 0;
- (void)flipToNewImage:(UIImage*)img {
AATileView *mySelf = self;
[UIView transitionWithView:self.imageView
duration:0.4
options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{
self.imageView.image = img;
} completion:^(BOOL finished) {
imageFlipIndex++;
if (imageFlipIndex < newImages.count) {
UIImage *newImg = [newImages objectAtIndex:idx];
[self flipToNewImage:newImg];
} else {
imageFlipIndex = 0;
}
NSLog(#"Finished flip");
}];
}
What happens if you try:
imageFlipIndex++;
if (imageFlipIndex < newImages.count) {
UIImage *newImg = [newImages objectAtIndex:idx];
[[self.gridSquareViews objectAtIndex:imageFlipIndex] performSelector:#selector(flipToNewImage:) withObject:newImg afterDelay:(0.05*idx)];
} else {
imageFlipIndex = 0;
}
inside the animations block?

UILabel set text is drawn over older one during Animation

I'm having trouble understanding what is going wrong if you can help me it would be great!
I have a UIlabel being animated by UIView AnimationWithDuration like a marquee... (I need to make this code work, I can't fork something elsewhere please don't provide marquee reps.)
During this animation I set a new text, unfortunately the former one is not replace but drawn over. I end up with two text drawn over each other. And if I try to set the text again it's possible that it get cleaned or it's possible that it get a third line drawn over.
I've tried the following code thinking it's probably a thread issue but it doesn't do anything
#synchronized (self.songLabel.text) {
self.songLabel.text = nil;
self.songLabel.text = [trackItems objectForKey:#"TrackName"];
}
I've set my songLabel getter and setters as atomic, it does not help either.
I don't know what to try... thank you for your help!
EDIT: Here is the code that take care of the animation
- (void)animateSongLabel
{
if (!self.player.rate) {
[UIView animateWithDuration:.5 animations:^{
self.songLabel.left = 25.f;
}];
return;
}
if (!self.canAnimateLabel) {
return;
}
self.animateLabel = NO;
[UIView animateWithDuration:.25 delay:0. options:UIViewAnimationOptionCurveLinear animations:^{
self.songLabel.left -= 15.f;
} completion:^(BOOL finished) {
self.animateLabel = YES;
if (self.songLabel.right < -10.f) {
self.songLabel.left = 320.f;
}
[self animateSongLabel];
}];
}

dynamically change images in an animation IOS

I have a requirement to animate images . I have a large number of images and this needs to be played as an video. In between playing sometimes i need to change some images as they will be updated at server. so playing should automatically update this new images .
I have tried using UIImageView. There we cannot control the animation.
I then tried CAKeyframeAnimation supplying image array to values property. I could play and pause the animation. But here also i cannot dynamically change the image while playing.
Can anyone help me in solving this problem.
Thanks
mia
The animation system on UIImageView is very limited. I would suggest that you make your own implementation with say 2 image view.
You would than change the image in one imageview and the fade in and out using UIView animateWithDuration
I have written a method for you. Please note: I have not tested it.
It assumes you have your frames in a array called 'frames' and have two UIIMageView placed on top of each other called 'imgv1' and 'imgv2'
-(void)performAnimationOfFrameAtIndex:(NSNumber*)indexNum
{
int index = [indexNum intValue];
if(index >= frames.count)
{
return;
}
UIImage *img = [frames objectAtIndex:index];
UIImageView *imgIn;
UIImageView *imgOut;
if(index % 2 == 0)
{
imgIn = imgv1;
imgOut = imgv2;
}
else {
imgIn = imgv2;
imgOut = imgv1;
}
imgIn.image = img;
[self.view sendSubviewToBack:imgIn];
[UIView animateWithDuration:0.1 animations:^{
imgIn.alpha = 1;
imgOut.alpha = 0;
} completion:^(BOOL finished) {
[self performSelectorOnMainThread:#selector(performAnimationOfFrameAtIndex:) withObject:[NSNumber numberWithInt:index+1] waitUntilDone:NO];
}];
}
Use two UIImageViews and swap them.
If you have a weak reference on the "animation" UIView, be sure to check if animation has finished in the completion block. Otherwise you may experience performance issues when you navigate to another view, recursive calls to animation method will continue!
- (void)animateImagesAtIndex:(NSNumber *)imageIdx {
// Do something here
[UIView animateWithDuration:1 animations:^{
// Do swap
}
completion:^(BOOL finished) {
if (finished) {
[self animateImagesAtIndex:imageIdx];
}
}];
}
UIImageView supports animations for a series of images. You only have to set the properties animationImages with an array of the images, and call the methods startAnimating and stopAnimating.

Resources