We have a custom view with a UIPickerView plus toolbar (216 + 44). At init time (viewDidLoad) this custom view is pushed below the screen using the following piece of code.
CGPoint newOrigin;
newOrigin.x = pickerViewOutlet.frame.size.width/2;
newOrigin.y = self.view.frame.size.height + ((pickerViewOutlet.frame.size.height)/2);
NSLog(#"%f,%f",self.view.frame.size.height,(pickerViewOutlet.frame.size.height)/2);
pickerViewOutlet.center = CGPointMake(newOrigin.x, newOrigin.y);
When a button is clicked this view is pulled up using the following piece of code.
[self.view bringSubviewToFront:pickerViewOutlet];
NSLog(#"tabbar %f",self.tabBarController.tabBar.frame.size.height);
CGPoint showOrigin;
showOrigin.x = pickerViewOutlet.frame.size.width/2;
showOrigin.y = pickerViewOutlet.center.y - pickerViewOutlet.frame.size.height;
//self.tabBarController.tabBar.frame.size.height ;
NSLog(#"showpicker %f,%f",pickerViewOutlet.center.y,pickerViewOutlet.frame.size.height);
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:0.5];
pickerViewOutlet.center=CGPointMake(showOrigin.x, showOrigin.y);
[UIView commitAnimations];
[pickerCtrlOutlet reloadAllComponents];
This works fine. However this does not work (part of the view is below the tab bar) in the presence of a bottom tab bar controller on the page even though the code is modified as
showOrigin.y = pickerViewOutlet.center.y - pickerViewOutlet.frame.size.height - self.tabBarController.tabBar.frame.size.height ;
However if the above code is modified to
showOrigin.y = pickerViewOutlet.center.y - pickerViewOutlet.frame.size.height - self.tabBarController.tabBar.frame.size.height - 90;
it works perfectly where the view is right above the tab bar.
As far as I know, in viewDidLoad the self.view is not yet added to the superview and thus the frame is not set to the correct sizes.
For example, you can design a UIView in InterfaceBuilder and it will have 320x460. When you add it to the superview it will actually become smaller because of the bottom tab bar. The auto-resizing mechanism helps in this matter.
So, I think you are positioning the picker view using the wrong values in viewDidLoad and then when you use a new position relative to its old one, it will still be wrong.
Here is how I would write this:
[self.view bringSubviewToFront:pickerViewOutlet];
NSLog(#"tabbar %f",self.tabBarController.tabBar.frame.size.height);
CGPoint showOrigin;
showOrigin.x = pickerViewOutlet.frame.size.width/2;
//Notice this line -----------
showOrigin.y = self.view.frame.size.height - pickerViewOutlet.frame.size.height / 2;
//self.tabBarController.tabBar.frame.size.height ;
NSLog(#"showpicker %f,%f",pickerViewOutlet.center.y,pickerViewOutlet.frame.size.height);
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:0.5];
pickerViewOutlet.center=CGPointMake(showOrigin.x, showOrigin.y);
[UIView commitAnimations];
[pickerCtrlOutlet reloadAllComponents];
Notice showOrigin.y = self.view.frame.size.height - ...
And (for extra points :) you can set the autoresizing masks for the picker view to Flexible Top (or, lock the bottom coordinate). If you do this, even if you position the picker view in viewDidLoad and then the self.view resizes, the pickerview will also change it's position.
Related
I want to animate a view to a specified origin when ever someone touched it and then comeback to its original position whenever user touched it again. I am using AutoLayout in my storyboard to align my views.
It first go upward with perfect position but does not come back to its original position perfectly.
I store the staring origin value in a CGFloat variable in my viewDidLoad method and then re assign that object to bring back the view.
Here is my code
Animation for upward (This is working perfect)
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.2];
CGRect frame = _pickLocationView.frame;
frame.origin.y = _cityLabelView.frame.origin.y;
_pickLocationView.frame = frame;
[UIView commitAnimations];
Animation for Downward. (This method has some issue)
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.2];
CGRect frame = _pickLocationView.frame;
frame.origin.y = kDefaultOriginOfPickLocationView;
_pickLocationView.frame = frame;
[UIView commitAnimations];
A suggestion:
Use the method - [self setNeedsLayout] before the animation starts.
The frame is not guaranteed to have its final dimensions and position in viewDidLoad. Use viewDidLayoutSubviews to set your original values instead. Note that viewDidLayoutSubviews can be called multiple times if the bounds of your view changes.
In your case you could also just save the original value just before you start the first animation.
I have a screen within an iPhone app that consist of a UITextView. This text view is contained within a UIScrollView. The purpose of the screen is for the user to type in text, and to optionally attach an image to what he is writing. Therefore, the screen also has a UIToolbar with a camera button at the bottom of the screen. The structure of the screen is as follows:
-View
--UIScrollView
---UITextView
--UIToolbar
---UIButton
When the user navigates to this screen, the viewDidAppear method assigns first responder to the uitextview element, so the keyboard shows up, which hides the toolbar and the camera button.
I would like the entire toolbar to re-draw itself right above the keyboard, and to position itself again at the bottom of the screen when the keyboard hides.
I have found related posts on SO (like this one). However, such methods introduce undesired behaviours. For example, implementing the solution in the article above, the toolbar does move with the keyboard, but the UIScrollView gets its frame.origin.y coordinate shifted way above the top of the screen, so it's impossible for the user to see what he is typing.
I have also tried to reset the frame of the toolbar, by adding it as an IBOutlet and using cgrectmake to reposition it. However, after several tries, the toolbar remains stuck at the bottom of the screen and hidden by the keyboard:
- (void) liftMainViewWhenKeybordAppears:(NSNotification*)aNotification{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:0.3];
[UIView setAnimationCurve:<#(UIViewAnimationCurve)#>]
CGRect frame = self.keyboardToolbar.frame;
frame.origin.y = self.keyboardToolbar.frame.origin.y - 280;
self.keyboardToolbar.frame = frame;
[UIView commitAnimations];
}
I have tried several iterations similar to the code above and they all fail at repositioning the toolbar.
So in short, what is the right way to float a toolbar right on top of a keyboard in a screen whose space is completely utilised by a uitextview element?
Thanks to RoryMcKinnel for the pointer. As the article referenced is in Swift, I thought I might paste the solution that worked for be on ObjC
- (void)keyboardWillShowNotification:(NSNotification *)notification{
NSDictionary *userInfo = notification.userInfo;
double animationDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGRect keyboardEndFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect convertedKeyboardFrame = [self.view convertRect:keyboardEndFrame fromView:self.view.window];
UIViewAnimationOptions rawAnimationCurve = (UIViewAnimationOptions)[userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue] << 16;
_toolBarBottomGuide.constant = CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(convertedKeyboardFrame);
[UIView animateWithDuration:animationDuration delay:0.0 options:rawAnimationCurve animations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
Bear in mind, this code did make the toolbar move as required, but the toolbar was not visible at all. It turned out that it was being hidden behind the UIScrollView. This was easily fixed by shifting the order between the scroll view and the toolbar element in the IB hierarchy.
The method above works for the keyboardWillShow event. You'll need to add the corresponding one for when the keyboard hides, like this:
- (void)keyboardWillHideNotification:(NSNotification *)notification{
NSDictionary *userInfo = notification.userInfo;
double animationDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGRect keyboardEndFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIViewAnimationOptions rawAnimationCurve = (UIViewAnimationOptions)[userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue] << 16;
_toolBarBottomGuide.constant = 0.0f;
[UIView animateWithDuration:animationDuration delay:0.0 options:rawAnimationCurve animations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
I've been searching around SO and have found some related posts, but none that have (yet) solved my problem.
I have a view setup in my storyboard, with a constraint hooked up through an IBOulet. Upon a certain action, the view should move up (or down to it's initial position). For the life of me, I cannot get it to work properly. Prior to autolayout, I ran this bit of code:
- (void)animateStarBackground:(NSString *)animationID
finished:(NSNumber *)finished
context:(void *)context
myView:(UIView *)myView
xPos:(float)xPos
yPos:(float)yPos {
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.2];
[UIView setAnimationDelay:0.5];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDelegate:self];
myView.center = CGPointMake(xPos, yPos);
[UIView commitAnimations];
}
It used to work great, but anyway... we all know that this doesn't work in Autolayout anymore. So I changed it to the following:
-(void)animateButton:(UIView *)myView {
[UIView animateWithDuration:8.0f animations:^{
_viewConstA.constant = 45.0;
[myView layoutIfNeeded];
}];
}
When the action occurs, instead of starting in it's initial position as set in the Storyboard, it starts off as a 1x1 pixel view in the top left hand corner and animates downward into it's final resting place (instead of moving up 15 pixels as I had intended it to).
Now, if I take out the [myView layoutIfNeeded]; portion, the action positions the view in exactly the right place... BUT it doesn't animate there. It's just instantly there as the view appears.
The first screen grab is the initial location.
2nd grab is what's supposed to be the views final resting place.
3rd is the view animating from the wrong spot and direction.
Thoughts / comments? What the heck am I doing wrong? I thought this would be so easy... I've got to be missing something obvious.
When I call the following block-based animation from viewDidLoad, it animates the frame from CGRectZero to the final location. But if I call this from viewDidAppear, it works just as expected.
- (void)animateMovementByConstraint
{
[UIView animateWithDuration:0.5
animations:^{
self.topLayoutConstraint.constant = 100;
[self.view layoutIfNeeded];
}];
}
(In this example, topLayoutConstraint is an IBOutlet that I defined in Interface Builder for the top vertical constraint on the control that I wanted to animate.)
With auto layout, since I've started initiating my animations in viewDidAppear, I haven't had any problems.
I have a tabbed application for iPad. On the first tab I have a separate menu with a pointer that highlights the currently active menu button. The pointer (a UIImage) is animated into position after tapping the button.
The problem is when you leave the highlighted image on any button that's not the original/default button and move to another tab in the application, then back again, the image has moved back to the default location. However, when you tap another button the image moves in the wrong direction. I think it's because the image moves back but the old coordinates are retained, or the other way around? It's a bit hit and miss and behaves differently testing in the simulator compared to directly on the iPad.
I think I need to start with absolute coordinates and stick with them throughout the process. But I can't get my head around how to achieve it with the CGAffine functions.
Here is my current code
- (IBAction)diplayForm1:(id)sender {
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.5];
buttonHighlightImage.transform = CGAffineTransformMakeTranslation(0, 0);
[UIView commitAnimations];
}
- (IBAction)diplayForm2:(id)sender {
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.5];
buttonHighlightImage.transform = CGAffineTransformMakeTranslation(0, 80);
[UIView commitAnimations];
}
- (IBAction)diplayForm3:(id)sender {
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.5];
buttonHighlightImage.transform = CGAffineTransformMakeTranslation(0, 160);
[UIView commitAnimations];
}
EDIT - - -
#Mundi I tried the code you suggested. I'm assuming by newFrame you mean define a new UIImageView and property which I did in the .h (I called it 'sfh'), then #synthesize in the .m.
This is my code in the .m
-(void)viewWillAppear:(BOOL)animated {
// The "pointer" and the "old" frame are the same so I thought it pointless to do
// selectedFormHighlight.frame = selectedFormHighlight.frame;
// I tried anyway and also tried this and many other combinations
selectedFormHighlight.frame = sfh.frame;
}
-(void)viewDidAppear:(BOOL)animated {
[UIView beginAnimations:nil context:NULL];
[UIView animateWithDuration:0.3 animations:^{
selectedFormHighlight.frame = CGRectMake(0,0,0,0);
}];
[UIView commitAnimations];
}
When I run this the pointer animates off the screen (top-left) and doesn't come back. I have tried every possible combination I can think of in viewWillAppear and viewDidAppear but it makes no difference. I tried removing the other lines in viewDidAppear and it still does the same thing.
I think you're suggesting to animate the pointer from the original (default) position when the user returns from another screen. I don't want that, it should simply remain in the same spot where they left it without animation. Or at least put it back there without them realizing it. The only animation is when they choose to tap a different button within that view.
The reason you are experiencing this is that the actual view object and its presentation layer get mixed up when the view is hidden and redisplayed. The solution is to change the view properties rather than animate the layers.
Thus, you do not need to use affine transforms. Just set the new position of the pointer view directly by changing its frame or center.
The more up-to-date format to do this is with block based animations:
[UIView animateWithDuration:0.5 animations:^{
pointer.frame = newFrame;
}];
Also, it seems very inefficient to have three methods just differing by one variable. You could just multiply the desired y position by 80.
Edit:
Another reason your button resets is because that is where it was put when the view is instantiated. Just set the position of your view in viewWillAppear of your view controller and then animate it to the new location in viewDidAppear.
I appreciate the help from #Mundi but couldn't get it to work that way. I eventually arrived at the following code.
// This initially sets the frame with a variable value that
// corresponds to the current child view
-(void)viewDidAppear:(BOOL)animated {
selectedFormHighlight.frame = CGRectMake(0, self.getY, 250, 4100);
}
// Button actions (converted to a single action instead of one for each button)
// This was done by referencing a tag added to each button (the button tags
// match the children's tags)
-(IBAction)goToForm:(id)sender {
UIButton *btn = (UIButton *)sender;
self.currentChild = self.formViewControllers[btn.tag];
[UIView beginAnimations.nil context:NULL];
[UIView animateWithDuration:0.3 animations:^{
selectedFormHighlight.frame = CGRectMake(0, self.getY, 250, 4100);
}];
[UIView commitAnimations];
sfh.frame = selectedFormHighlight.frame;
}
// This returns the 'y' (vertical center position) of the pointer
// frame for each of the child views
-(int)getY {
switch(_currentChild.view.tag) {
case 1:
return -2005;
break;
case 2:
return -1925;
break;
default:
return -1845;
break;
}
}
I have two UIImageView filling the view controller, the first is filling the top half, and the second for the bottom half. I set them directly in the storyboard file.
In the viewDidLoad method, i am setting code in order to perform animation for the two UIImageView, so that it will look like a basket which gets opened (the first UIImageView moved to the top until it gets out of the view, and the second UIImageView moved to the bottom of the view until it gets out of the view).
This is my code so far:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CGRect basketTopFrame = self.basketTop.frame;
basketTopFrame.origin.y = -basketTopFrame.size.height;
CGRect basketBottomFrame = self.basketBottom.frame;
basketBottomFrame.origin.y = self.view.bounds.size.height;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
[UIView setAnimationDelay:1.0];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
self.basketTop.frame = basketTopFrame;
self.basketBottom.frame = basketBottomFrame;
[UIView commitAnimations];
}
But the behavior is different than i was expecting, the top frame is not moving, and the bottom frame is animated from top left to bottom (origin position, and not moving out of the view).
This is UIImageViews position as i set in the storyboard:
This is the UIImageViews when i launch the app:
And this is the UIImageViews when the app finish the animation(viewDidLoad):
Please note that this code works in Xcode 4.2, but since upgrading to Xcode 4.5 and using Storyboard, i begin getting this issue. Thanx in advance.
You may have auto layout enabled on your storyboard. In this case the view components will not have a valid frame value at viewDidLoad, so your code won't work.
You can disable auto layout by selecting the file inspector in the storyboard and unchecking "Use Autolayout".
In any case, that is the wrong method to be starting an animation from. Try moving the code to viewDidAppear.
Once you have "AutoLayout" turned off and if you are targeting IOS 4 and later than block based UIView animation is recommended by Apple as it simplifies the process and you can do things in one line. Please check UIView Reference for further information.
In your case , whether you plan to do the animation in ViewDidLoad / ViewWillAppear, it can be done in the following way using block based animation:
CGRect basketTopFrame = self.boxTop.frame;
CGRect basketBottomFrame = self.boxBottom.frame;
[UIView animateWithDuration: 0.5 delay:0.1 options:UIViewAnimationOptionCurveEaseOut animations:^{
[self.boxTop setFrame:CGRectMake(basketTopFrame.origin.x, -basketTopFrame.size.height, basketTopFrame.size.width, basketTopFrame.size.height)];
[self.boxBottom setFrame:CGRectMake(basketBottomFrame.origin.x, self.view.bounds.size.height, basketBottomFrame.size.width, basketBottomFrame.size.height)];
} completion:^(BOOL finished) {
// TODO : Do any additional stuffs.
}];