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.
Related
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?
I've built an app where a UISegmentedControl needs to be moved corresponding to its selected index. I'm using this code to achieve so:
- (IBAction)segmentControlAction:(id)sender {
// Change which container will be visible
int selectedIndex = self.overviewSegmentControl.selectedSegmentIndex;
if (selectedIndex == 0) {
// Show details and hide reviews & related
// SHOW THE DETAILS
[self showDetails];
} else if (selectedIndex == 1) {
// Show Reviews and hide details & related
// SHOW THE REVIEWS
[self showOther];
} else if (selectedIndex == 2) {
// Show related and hide details & reviews
}
}
-(void)showOther {
// Animate the reviews
[UIView animateWithDuration:.5 delay:0 usingSpringWithDamping:.9 initialSpringVelocity:1 options:UIViewAnimationOptionTransitionNone animations:^{
// Hide details controls
self.profileImageView.alpha = 0;
self.seperatorImageView.alpha = 0;
self.byLabel.alpha = 0;
self.authorLabel.alpha = 0;
// Move segmentControl
[self.overviewSegmentControl setFrame:CGRectMake(self.overviewSegmentControl.frame.origin.x, self.previewImageView.frame.size.height + 8, self.overviewSegmentControl.frame.size.width, self.overviewSegmentControl.frame.size.height)];
}completion:^(BOOL finished) {
// Completed
}];
}
-(void)showDetails{
// Animate the details
[UIView animateWithDuration:.5 delay:0 usingSpringWithDamping:.9 initialSpringVelocity:1 options:UIViewAnimationOptionTransitionNone animations:^{
// Move segmentControl
[self.overviewSegmentControl setFrame:CGRectMake(self.overviewSegmentControl.frame.origin.x, self.previewImageView.frame.size.height + 85, self.overviewSegmentControl.frame.size.width, self.overviewSegmentControl.frame.size.height)];
// Hide details controls
self.profileImageView.alpha = 1;
self.seperatorImageView.alpha = 1;
self.byLabel.alpha = 1;
self.authorLabel.alpha = 1;
}completion:^(BOOL finished) {
// Completed
}];
}
This code moves the UISegmentedControl, but it reverts it to it's original position before performing the actual move. This results in a weird jumping of the control.
My UIViewController:
Here you can see my constraints:
Can someone please explain me how this works and how to move controls with constraints rather than setting frames?
Thanks!
Erik
You can programmaticly modify the ".constant" properties of constraint objects at runtime to set positioning and/or size values. You do this in the updateConstraints method of your view controller.
You can bind the constraints in your storyboard to properties in your code the same way you bind other objects: control-click-drag from the object into your source code window. Then, you can access them at runtime.
Also, make sure you set translatesAutoresizingMaskIntoConstraints to FALSE or you might get conflict constraints.
For example you can "set" a constraint to top from your segment controller: "Top space" and modify this constraint accordingly.
Here you have an example testMovingControll
i am new to IOS programming. i have a background image on my screen. What i want is when app launches screen shows with the background image and then after 1 sec 2 buttons comes up from right side of the screen. how can i do this. here is the image what i am trying to do
button 1 and button2 have to be invisible first. after 1 second they comes up from right side.
if there are any tutorials related to this kind of animation then please share
You can use simple UIView Animations to achieve this.
Set your button x origin any value greater than screen size so that it is not visible first. Then reset the button frame to a visible value with view animation. For example call the following function in your viewWillAppear: method.
- (void) animateButton {
// Setting button origin to value greater than the view width.
CGRect frame = button.frame;
frame.origin.x = self.view.frame.size.width+10;// or set any value > 320
button.frame = frame;
[UIView animateWithDuration:0.5
delay:2.0
options: UIViewAnimationCurveEaseOut
animations:^{
button.frame = CGRectMake(20, 100, 60, 25) ;// Any value according to your requirement
}
completion:^(BOOL finished){
NSLog(#"Done!");
}];
}
Refer Tutorial
You can do it this way, please note that, if you want to animate the way your button shows up, you must hide them using alpha = 0.0f (and not hidden = true). This way, you will have a smooth showing up animation !
Here it is :
First, on your viewDidLoad of your UIViewController, you have to set up a NSTimer of 1sec, then let it call the method that will let your buttons appear :
- (void) viewDidLoad {
[super viewDidLoad];
// Do your init stuff here
// We will call your buttons as if they are declared as properties of your class, ask if you don't know how to set them
self.button1.alpha = 0.0f;
self.button2.alpha = 0.0f;
// This will wait 1 sec until calling showButtons, where we will do the trick
NSTimeInterval timeInterval = 1.0;
[NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:#selector(showButtons) userInfo:nil repeats:NO];
}
- (void) showButtons {
[UIView beginAnimations:#"buttonShowUp" context:nil];
[UIView setAnimationDuration:0.5]; // Set it as you want, it's the length of your animation
self.button1.alpha = 1.0f;
self.button2.alpha = 1.0f;
// If you want to move them from right to left, here is how we gonna do it :
float move = 100.0f; // Set it the way you want it
self.button1.frame = CGRectMake(self.button1.frame.origin.x - move,
self.button1.frame.origin.y,
self.button1.frame.size.width,
self.button1.frame.size.height);
self.button2.frame = CGRectMake(self.button2.frame.origin.x - move,
self.button2.frame.origin.y,
self.button2.frame.size.width,
self.button2.frame.size.height);
// If you want to set them in the exact center of your view, you can replace
// self.button1.frame.origin.x - move
// by the following
// (self.view.center - self.button1.frame.size.width/2)
// This way, your button will be centered horizontally
[UIView commitAnimations];
}
Ask if you have any questions !
Set the buttons' x co-ordinates such that the button is just outside the screen on the right side.
And then try the following in your viewDidLoad()
[UIView animateWithDuration:0.5
delay:1.0
options:NO
animations:^{ change your button x co- ordinate here to the co-ordinate you need it on finally}
completion:NULL];
When implementing UIView animateWithDuration:animations:completion: Class method, I encountered with a scenario I couldn't figure our how to handle.
I have a UIImageView that moves across the screen. Upon a certain event, it should change direction, and move to a new position. But(!) It seems that if this event happens, instead of just changing directions and moving to the new position, it 'jumps' to the original 'end position' and begin moving to the new position from there.
I'm not sure how I can instruct the 'moving object', upon completion == NO, to capture its current position and start animating from there, instead of jumping to the predetermined end position.
Here is the code:
- (IBAction)goHere:(UIButton *)sender
{
CGPoint movingEndPoint;
if ( sender == self.btn1 )
{
movingEndPoint = CGPointMake( sender.center.x + 40, sender.center.y + 40 );
}
if ( sender == self.btn2 )
{
movingEndPoint = CGPointMake( sender.center.x - 40, sender.center.y - 40 );
}
[UIView animateWithDuration:3
animations:^ {
self.movingObj.center = movingEndPoint;
}
completion:^(BOOL completion) {
if (completion == NO)
{
//How to express "New Position = Current Position"?
}
}];
}
This behavior has been remedied in iOS 8 (where it picks up the new animation using the object's current position and direction), but in earlier iOS versions, the easiest fix is to use the rendition of animateWithDuration with the options parameter, and include the UIViewAnimationOptionBeginFromCurrentState option.
Alternatively, you can use the view's layer's presentationLayer to get the current position mid-animation. For example,
CALayer *layer = self.movingObj.layer.presentationLayer;
CGRect currentFrame = layer.frame;
You can then set then stop the animations (e.g. [self.movingObj.layer removeAllAnimations]) and then set the frame of the view using this currentFrame before initiating the next animation.
Instead of doing it in completion block,
add this code in your event before giving new position.
Lets say you event method is eventMethod,
your modified code should be
[UIView animateWithDuration:5
animations:^ {
self.movingObj.center = CGPointMake(400, 400); //old final position
}
completion:^(BOOL completion) {
}];
-(void)eventMethod{
_movingObj.frame =[_movingObj.layer.presentationLayer frame];
[UIView animateWithDuration:2
animations:^ {
self.movingObj.center = CGPointMake(0, 0); //new final position
}
completion:^(BOOL completion) {
}];
}
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];
}];
}