Is there anyway to detect that the tabbar of a UITabBarController is going to appear or disappear? I want to make an animation simultaneously with the animation that shows/hides the tabbar.
I haven't find any way to detect this event. The property "hidden" of the tabbar is not an option because it changes its value once the animation has finished
The solution was to use the method in the view controller didUpdateFocusInContext:withAnimationCoordinator: with this code:
static NSString *kUITabBarButtonClassName = #"UITabBarButton";
NSString *prevFocusViewClassName = NSStringFromClass([context.previouslyFocusedView class]);
NSString *nextFocusedView = NSStringFromClass([context.nextFocusedView class]);
// The tabbar is going to disappear
if ([prevFocusViewClassName isEqualToString:kUITabBarButtonClassName] &&
![nextFocusedView isEqualToString:kUITabBarButtonClassName]) {
[self.view layoutIfNeeded];
self.constraintScrollViewCenterY.constant -= self.tabBarController.tabBar.frame.size.height;
[coordinator addCoordinatedAnimations:^{
[self.view layoutIfNeeded];
} completion:nil];
// The tabbar is going to appear
} else if (![prevFocusViewClassName isEqualToString:kUITabBarButtonClassName] &&
[nextFocusedView isEqualToString:kUITabBarButtonClassName]) {
[self.view layoutIfNeeded];
self.constraintScrollViewCenterY.constant += self.tabBarController.tabBar.frame.size.height;
[coordinator addCoordinatedAnimations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
where self.constraintScrollViewCenterY is a constraint related to the vertical alignment of the view I want to move according to the tabbar movement
Note: The use of class name (kUITabBarButtonClassName) instead of [... class] method is due to UITabBarButton is a private class
I have a simple KeyboardAdjuster class that is a property of my form views. If one of the form fields is hidden by the keyboard, then entering that field will have an animation to move the whole frame's origin.y up, so that the field appears above the keyboard. A very common approach. It also has a few complexities like calculating how much to scroll by when navigating between fields, but that's not important right now. . . I've been using this utility class since iOS5.
Example Form:
The problem:
On iOS8 it has simply stopped working.
When animating the frame starts by snapping in the opposite direction exactly the amount that its supposed to scroll by. And then scrolling back to the origin.
I tried commenting the animation part out, and simply setting the frame. No effect.
For example if the frame is supposed to be: {0, -127, 320, 480} then it will simply stay at {0, 0, 320, 480}
Why doesn't this work on iOS8? Has something changed that I've missed?
About the views:
My views are hand-coded, they're a sub-class of a simple form base-view. (Contains keyboard adjuster and a scroll-view). The other elements are added with initial frames of CGRectZero and then laid out manually in layoutSubviews
How the view/controller created:
There is a RootViewController that acts as a container controller (UIView containment). It:
Has a main navigation controller
Has a container to present / dismiss a hamburger menu for the nav controller's top views (these can change).
Has a container to present overlays with a custom bounce animation.
So the view is created as follows:
- (instancetype)initWithView:(INFAcceptGiftView *)view offerDao:(id <INFOfferDao>)offerDao
locationTracker:(INFLocationTracker *)locationTracker
{
self = [super init];
if (self)
{
self.view = view;
_offerDao = offerDao;
_locationTracker = locationTracker;
}
return self;
}
What triggers the keyboard animation?:
The UIView is a sub-class of form base view, which is a UITextFieldDelegate:
interface INFFormBaseView : UIView <UITextFieldDelegate, INFInputAccessoryDelegate>
{
UIResponder *_currentResponder;
INFInputValidator *_validator;
}
When a field is entered:
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
[_validator dismissMessages];
[_keyboardAdjuster scrollToAccommodateField:textField];
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
[_keyboardAdjuster scrollToAccommodateField:nil];
}
I have a different solution for you which works on both iOS 7 & 8 and Auto Layout.
In my example I have two UITextFields which I move and hide depending on their position and the position of the keyboard. In this particular case, I switch the UITextFields between them and hide the inactive one.
In viewDidLoad you register for the following notifications:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardShowed:)
name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardHidden:)
name:UIKeyboardWillHideNotification object:nil];
After that you grab a hold of the default frames of your views, in this case the two UITextFields:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
/*** FOR AUTOLAYOUT MODIFICATIONS & ADDITIONS # RUNTIME ***/
self.mailTextFieldDefaultFrame = self.mailTextField.frame;
self.passwordTextFieldDefaultFrame = self.passwordTextField.frame;
}
And when you receive UIKeyboardWillShowNotification you'll start moving your views:
- (void) keyboardShowed:(NSNotification*)notification {
//GET KEYBOARD FRAME
CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
//CONVERT KEYBOARD FRAME TO MATCH OUR COORDINATE SYSTEM (FOR UPSIDEDOWN ROTATION)
CGRect convertedFrame = [self.view convertRect:keyboardFrame fromView:self.view.window];
if ([self.mailTextField isFirstResponder]) {
[UIView transitionWithView:self.mailTextField
duration:.3f
options:UIViewAnimationOptionCurveLinear
animations:^{
self.mailTextField.alpha = 1.0f;
self.mailTextField.frame = CGRectMake(self.mailTextField.frame.origin.x,
convertedFrame.origin.y -
self.mailTextField.frame.size.height - 25,
self.mailTextField.frame.size.width,
self.mailTextField.frame.size.height);
self.passwordTextField.alpha = 0.0f;
}
completion:nil];
} else if ([self.passwordTextField isFirstResponder]) {
[UIView transitionWithView:self.passwordTextField
duration:.3f
options:UIViewAnimationOptionCurveLinear
animations:^{
self.passwordTextField.alpha = 1.0f;
self.mailTextField.frame = self.passwordTextField.frame;
self.passwordTextField.frame = CGRectMake(self.passwordTextField.frame.origin.x,
convertedFrame.origin.y -
self.passwordTextField.frame.size.height - 25,
self.passwordTextField.frame.size.width,
self.passwordTextField.frame.size.height);
self.mailTextField.alpha = 0.0f;
}
completion:(void (^)(BOOL finished)) ^{
}];
}
And when you hide the keyboard:
- (void) keyboardHidden:(NSNotification*)notification {
//RESTORE ORIGINAL STATE OF TEXTFIELDS
[UIView transitionWithView:self.view
duration:.3f
options:UIViewAnimationOptionCurveLinear
animations:^{
self.mailTextField.frame = self.mailTextFieldDefaultFrame;
self.passwordTextField.frame = self.passwordTextFieldDefaultFrame;
self.mailTextField.alpha = 1.0f;
self.passwordTextField.alpha = 1.0f;
}
completion:nil];
}
Here's the solution, posting in case it helps someone.
I mentioned above that I'm using UIView containment, so I have a root view controller that:
Contains a UINavigationController (the root view is replaceable).
Contains a Menu Controller (dealloc'd when not in use)
Presents an overlay with custom animation
My root view had layout subviews as follows:
- (void)layoutSubviews
{
[super layoutSubviews];
[_mainContentViewContainer setFrame:self.bounds];
}
This behaved the way that I wanted it to pre iOS8, but not afterwards. Technically it appears that iOS8 is doing the right thing - I should only be laying out the _mainConentViewContainer on startup or orientation change.
I am using Autolayout and animating by changing the constraints, however, in iOS7 the view simply jumps to the end position - in iOS6 I get a nice animation.
Is should be noted these views are UICollectionViews and I have checked the Storyboard and there are no Layout errors.
All I can think is there is something and am or am not setting on the Storyboard or something that I am doing wrong with the Constant settings in the Storyboard.
primaryMenuYContraints.constant = BUTTOMX;;
leftMenuYContraints.constant = 136.0f;
leftMenuBottomConstraint.constant = 5.0f;
[UIView animateWithDuration:0.7f
delay:0.0f
options:UIViewAnimationOptionCurveLinear
animations:^
{
// Move in menus
[self.primaryOptionCollection layoutIfNeeded];
[self.menuOptionCollection layoutIfNeeded];
}
completion:^(BOOL finished)
{
}];
I changed to and now works in both iOS7 and 6, still not sure why it does/did it though! I still think I am setting something up wrong in the Storyboard. I am add another view (nothing to do with this lot) programmatically so I believe that is based around frames until I convert it (which I am not doing).
primaryMenuYContraints.constant = BUTTOMX;;
leftMenuYContraints.constant = 136.0f;
leftMenuBottomConstraint.constant = 5.0f;
[UIView animateWithDuration:0.7f
delay:0.0f
options:UIViewAnimationOptionCurveLinear
animations:^
{
// Move in menus
[self.view layoutIfNeeded];
}
completion:^(BOOL finished)
{
}];
I want to toggle the visibility of the status bar on tap, just like it does in the Photos app.
Prior to iOS 7, this code worked well:
-(void)setStatusBarIsHidden:(BOOL)statusBarIsHidden {
_statusBarIsHidden = statusBarIsHidden;
if (statusBarIsHidden == YES) {
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationFade];
}else{
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade];
}
}
But I can't get it to work in iOS 7. All the answers that I found only offer suggestions for permanently hiding the bar but not toggling.
Yet, there must be a way since Photos does it.
By default on iOS 7 or above, to hide the status bar for a specific view controller, do the following:
if the view controller you want to hide the status bar with is being presented modally and the modalPresentationStyle is not UIModalPresentationFullScreen, manually set modalPresentationCapturesStatusBarAppearance to YES on the presented controller before it is presented (e.g. in -presentViewController:animated:completion or -prepareForSegue: if you're using storyboards)
override -prefersStatusBarHidden in the presented controller and return an appropriate value
call setNeedsStatusBarAppearanceUpdate on the presented controller
If you want to animate it's appearance or disappearance, do step three within an animation block:
[UIView animateWithDuration:0.33 animations:^{
[self setNeedsStatusBarAppearanceUpdate];
}];
You can also set the style of animation by returning an appropriate UIStatusBarAnimation value from -preferredStatusBarUpdateAnimation in the presented controller.
First set View controller-based status bar appearance in Info.plist to YES
This Swift Example shows how to toggle the StatusBar with an Animation, after pressing a Button.
import UIKit
class ToggleStatusBarViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func prefersStatusBarHidden() -> Bool {
return !UIApplication.sharedApplication().statusBarHidden
}
override func preferredStatusBarUpdateAnimation() -> UIStatusBarAnimation {
return UIStatusBarAnimation.Slide
}
#IBAction func toggleStatusBar(sender: UIButton) {
UIView.animateWithDuration(0.5,
animations: {
self.setNeedsStatusBarAppearanceUpdate()
})
}
}
I was able to simplify #Jon's answer and still get behavior indistinguishable from the Photos app on iOS 7. It looks like the delayed update when showing isn't necessary.
- (IBAction)toggleUI:(id)sender {
self.hidesUI = !self.hidesUI;
CGRect barFrame = self.navigationController.navigationBar.frame;
CGFloat alpha = (self.hidesUI) ? 0.0 : 1.0;
[UIView animateWithDuration:0.33 animations:^{
[self setNeedsStatusBarAppearanceUpdate];
self.navigationController.navigationBar.alpha = alpha;
}];
self.navigationController.navigationBar.frame = CGRectZero;
self.navigationController.navigationBar.frame = barFrame;
}
- (BOOL)prefersStatusBarHidden {
return self.hidesUI;
}
This might be considered a bit of a hack but it's the closest I've come to reproducing the effect. There's still one minor issue. When fading out, you can see the navigation bar being resized from the top. It's subtle enough but still not a perfect fade. If anyone knows how to fix it, let me know!
- (BOOL)prefersStatusBarHidden {
if (_controlsAreHidden == YES)
return YES;
else
return NO;
}
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation {
return UIStatusBarAnimationFade;
}
-(void)setControlsAreHidden:(BOOL)controlsAreHidden {
_controlsAreHidden = controlsAreHidden;
if (controlsAreHidden == YES) {
// fade out
//
CGRect barFrame = self.navigationController.navigationBar.frame;
[UIView animateWithDuration:0.3 animations:^ {
[self setNeedsStatusBarAppearanceUpdate];
self.navigationController.navigationBar.alpha = 0;
}];
self.navigationController.navigationBar.frame = CGRectMake(0, 20, barFrame.size.width, 44);
}else{
// fade in
//
CGRect barFrame = self.navigationController.navigationBar.frame;
self.navigationController.navigationBar.frame = CGRectMake(0, 20, barFrame.size.width, 64);
[UIView animateWithDuration:0.3 animations:^ {
[self setNeedsStatusBarAppearanceUpdate];
self.navigationController.navigationBar.alpha = 1;
}];
}
}
This code works perfectly fine:
-(void)setControlsAreHidden:(BOOL)controlsAreHidden {
if (_controlsAreHidden == controlsAreHidden)
return;
_controlsAreHidden = controlsAreHidden;
UINavigationBar * navigationBar = self.navigationController.navigationBar;
if (controlsAreHidden == YES) {
// fade out
//
CGRect barFrame = self.navigationController.navigationBar.frame;
[UIView animateWithDuration:0.3 animations:^ {
[self setNeedsStatusBarAppearanceUpdate];
self.navigationController.navigationBar.alpha = 0;
}];
self.navigationController.navigationBar.frame = CGRectZero;
self.navigationController.navigationBar.frame = CGRectMake(0, 20, barFrame.size.width, 44);
} else {
// fade in
//
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^ {
[self setNeedsStatusBarAppearanceUpdate];
}];
double delayInSeconds = 0.01;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self.navigationController setNavigationBarHidden:NO animated:NO];
navigationBar.alpha = 0;
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^ {
navigationBar.alpha = 1;
}];
});
}
}
Actually there is now need to mess with navigation bar frames. You can achieve smooth animation just by using 2 separate animation blocks. Something like this should work just fine.
#property (nonatomic, assign) BOOL controlsShouldBeHidden;
...
- (void)setControlsHidden:(BOOL)hidden animated:(BOOL)animated {
if (self.controlsShouldBeHidden == hidden) {
return;
}
self.controlsShouldBeHidden = hidden;
NSTimeInterval duration = animated ? 0.3 : 0.0;
[UIView animateWithDuration:duration animations:^(void) {
[self setNeedsStatusBarAppearanceUpdate];
}];
[UIView animateWithDuration:duration animations:^(void) {
CGFloat alpha = hidden ? 0 : 1;
[self.navigationController.navigationBar setAlpha:alpha];
}];
}
- (BOOL)prefersStatusBarHidden {
return self.controlsShouldBeHidden;
}
For compatibility with iOS 6 just make sure to check [self respondsToSelector:#selector(setNeedsStatusBarAppearanceUpdate)]
The way to resolve this depends on the value of the "View controller-based status bar appearance" setting in your app's plist.
If "View controller-based status bar appearance" is NO in your plist, then this code should work:
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationFade];
If "View controller-based status bar appearance" is on, in your view controllers, add this method:
- (BOOL) prefersStatusBarHidden {
// I've hardcoded to YES here, but you can return a dynamic value to meet your needs for toggling
return YES;
}
For toggling, when you want to change whether the status bar is hidden/shown based on the value of the above method, your view controller can call the setNeedsStatusBarAppearanceUpdate method.
To correct this issue with navigation bar sliding up when fading, you should add the following code:
self.navigationController.navigationBar.frame = CGRectZero;
into your "fade in" section before the following code line:
self.navigationController.navigationBar.frame = CGRectMake(0, 20, barFrame.size.width, 64);
This is necessary because the frame is the same and setting the same frame will be ignored and will not stop the navigation bar from sliding. Therefore you need to change the frame to something different and then set it again to the correct frame to trigger the change.
So, here's an interesting little problem I've had to deal with. I coded a navigationBar to be translucent and the view underneath to be fullScreen. When I load the view, I can tap on a clear button in the view to "activate" an animation that fades in the bar and other ui elements.
When I rotate the device WITH THE UI ELEMENTS VISIBLE it works perfectly.
But if I tap again to "turn off" the elements with a fade out animation, then rotate, it pushes the naivgationbar up into the status bar.
I don't understand why this happens. I don't want to turn off the statusBar, but if I have to, I will. Can anyone help me with the bar's autorotation positioning?
EDIT SOLVED
SOLVED, this code animates the 20 pixels needed to move the bar down.
- (void)showToolbar
{
if (toolbar.hidden == YES)
{
[self.navigationController.view layoutSubviews];
[UIView animateWithDuration:0.25 delay:0.0
options:UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction
animations:^(void)
{
[[UIApplication sharedApplication]setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade];
toolbar.hidden = NO;
toolbar.alpha = 1.0f;
self.navigationController.navigationBar.alpha = 1.0f;
CGRect frame = self.navigationController.navigationBar.frame;
frame.origin.y = 20.0;
self.navigationController.navigationBar.frame = frame;
}
completion:NULL
];
}
if ([self.navigationController.navigationBar isHidden]) {
[self.navigationController setNavigationBarHidden:NO animated:NO];
}
}