I am designing an iOS app in swift, and I am having some difficulty with animations during a controller transition. Specifically, I've implemented a UINavigationControllerDelegate, to listen for when a certain view is pushed. When this view is pushed, I want to hide a bar at the bottom of the screen. My code is working almost perfectly, however whenever I begin an animation on the height of the navigation controller, the current view (which is being removed) animates its height correctly, but the new controller which is being pushed already has the new height from the animation. To put some code to it, the following function is called from my UINavigationControllerDelegate's willShow viewController function:
func animatePlayerVisibility(_ visible: Bool) {
if visible == showingPlayer {
return
}
showingPlayer = visible
let height: CGFloat = visible ? 56.0 : 0.0
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.35) {
self.playerHeight.constant = height
self.viewBottom.constant = height
self.view.layoutIfNeeded()
}
}
'playerHeight' is an IBOutlet to a constraint on the height of the player container view. 'viewBottom' is also an IBOutlet constraint between the bottom of the top container view and the bottom of the screen. Essentially, as long as these two constraints are animated together, it should look nice.
To help visualize the graphical bug, I edited this line
self.viewBottom.constant = height
to
self.viewBottom.constant = height * 2.0
I have created an imgur album of the actual wrong behavior in action:
http://imgur.com/a/znAim
As you can see, the old view controller animates properly, when the new controller already has the new animated size.
Here is the layout of my storyboard:
Any help would be really appreciated. I've been trying to fix this for a while with no success.
EDIT: The view of the animation without the *2 applied.
https://imgur.com/a/2a5Sw
Have you thought about not using UINavigationController? Maybe it will be easier to use ChildViewControllers mechanism. Then with it you can use a powerful autolayouts and have more control over animation (in your case height)
More info about this here
I've created a nice little sample project you can find here!
There are a number of things that could be going wrong, and since I haven't looked over your project personally it's likely I organized things very differently in my sample, but hopefully you will understand it. I think the big thing is that I added a constraint in storyboard to the navigationController's container to the bottom of the root viewController. I don't adjust the height of this container at all when I animate.
Related
What I want to to: I want to drag down the whole view of a viewController to dismiss to the parent viewController using a pan gesture recognizer.
The Problem: When I drag the view down, the navigationBar decreases its height and does not look good. When the view returns to its original position, the navigationBar returns to the default size. I want the navigationBar to stay at its size. I also tried to use the new large titles and some other properties of the navigationController/-bar, but that did not solve it.
Note: Everything worked fine already before iOS 11.
My code:
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(dragViewDown(_:)))
navigationController!.view.addGestureRecognizer(panGesture)
}
#IBAction func dragViewDown(_ gesture: UIPanGestureRecognizer) {
if let dragView = gesture.view {
let translation = gesture.translation(in: dragView)
dragView.center.y = (dragView.center.y + translation.y)
gesture.setTranslation(CGPoint.zero, in: dragView)
}
}
This test project only has one viewController and does not provide the dismissal, but the problem is the same as in my working project.
I also uploaded the project to GitHub: https://github.com/maddinK7/navitationBar-pull-down-problem
Does anyone have an idea how to solve this? Thanks in advance.
I want the navigationBar to stay at its size
It is staying at its size. If you check the navigation bar's bounds size height before, during, and after the drag, you will see that it remains the same (probably 44) at all times. What's changing is the drawing extension that causes the drawing of the nav bar to extend up behind the status bar. It can't do that when you pull the whole thing away from the top of the screen, because it is not at the top next to the status bar any more. iOS 11 is more strict about the way it performs this drawing extension, probably because it has to do it in a special way on the iPhone X.
So, let's make sure you're doing this correctly:
Make sure that the navigation bar has a top constraint pinned to the safe area layout guide's top, with a constant of zero.
Make sure that the navigation bar has a delegate that returns .topAttached from position(forBar:).
If you are doing both those things and it doesn't help, you'll have to implement this in some other way entirely. Making the view directly draggable like this, without a custom parent view controller, was always dubious.
When UINavigationController attached top, system will add safe area top margin in the navigation background.
(NOTICE: Background margin will not changed when offset value is between 1 and 0)
So you have to handle attached/detached top event by handle gesture offset to change the right offset and content insets.
You can try the solution in my lib example. ;)
My example include UITableViewController in the UINavigationController, so it will relatively complex.
https://github.com/showang/OverlayModalViewController
What is the best approach for attaching a UIButton on top of UIScrollView or UITableView so when the view is scrolled, the button stays in its place.
Here examples below:
UIButton stays in the right bottom corner when the view is scrolled.
google+ app example
yahoo mail app example
I think this should work. Lay Out your button in a view that is outside of the tableviewcontroller. Then drag an outlet to the tableviewcontroller file. Then add it in code. This code would hold it at the top of the screen.
#IBOutlet var buttonView: UIView!
override func viewDidLayoutSubviews() {
self.view.addSubview(buttonView)
}
override func scrollViewDidScroll(scrollView: UIScrollView) {
var rect = self.buttonView.frame
rect.origin.y = max(0,scrollView.contentOffset.y + scrollView.contentInset.top)
self.buttonView.frame = rect
}
Thank you all for great answers!
I got it worked through storyboard by moving the button from scrollView to View itself. That way it's attached on UIView and it's independent of scrollview.
storyboard snapshot
So now the structure is:
- View
- ScrollView
- Button
Before it was:
- View
- ScrollView
- Button
There are many ways to go about doing this but two that I use most often are as follows.
One approach is embedding the view controller within a navigation controller. This will set a bar on the top and bottom if you choose that you can place bar button items upon.
Another approach is to place a UIView along the top and snap the constraints to the left, right, and top with 0 no-margin. Then set the height. I usually use 40px for the height but you can use what is applicable to your needs. After that you can place a button in that UIView and then set constraints on it to keep in in place.
In my experience, this isn't reliably possible to do with the scrollView itself.
My solution is usually to put anything that needs to float above the tableView/scrollView in a plain ViewController that also contains the tableView/scrollView parent.
If you're using storyboards with a UITableViewController scene, this will likely mean you need to use another scene with UIViewController with a container that has your UITableViewController.
For UITableView use tableHeaderView. For UIScrollView you need to create a separate view not in the scroll view's hierarchy.
Another solution is to put your UIButton in a UIToolbar, and then make the toolbar a child of the UINavigationController's view. After that, in viewDidLayoutSubviews, you can set the rect of the toolbar to sit just below the navigation bar and offset the top of the UIScrollView or UITableView.
Add button which you want in the storyboard.
Design your scrollview
self.view.sendSubviewToBack(scrollViewObj)(in the code)
This worked for me.
I'm having a problem with the prompt on a UINavigationItem that I just can't resolve...
I have a master and a detail view controller. When I push from the master to the detail a prompt is shown on the detail view controller:
However, when I pop back to the master view controller, the view isn't resized and the window shows through (the window has been coloured red):
This only happens on iOS7, on iOS6 the view resizes as expected.
I've tried a few things such as setting the prompt to nil in viewWillDisappear or viewDidDisappear but nothing seems to fix it.
If I set the navigation bar in the navigation controller to translucent it does fix this - unfortunately that's not an option.
I've created a very small example project here which demonstrates the issue: https://github.com/InsertWittyName/NavigationItemPrompt
Thanks in advance for any help!
A solution I can think of is to subclass the UIView of the master, and implement viewDidMoveToSuperview to set the frame of the view to be from the navigation bar's height to the end of the superview. Since the navigation bar is not translucent, your job is easier, as you don't have to take into account layout guides and content insets.
A few things to notice. When pushing and popping, the system moves your view controller's view into another superview for the animation and then returns it to the navigation controller's private view hierarchy. Also, when a view goes outside of the view hierarchy, the superview becomes nil.
Here is an example implementation:
#interface LNView : UIView
#end
#implementation LNView
- (void)viewDidMoveToSuperview
{
[super viewDidMoveToSuperview];
if(self.superview != nil)
{
CGRect rect = self.superview.bounds;
rect.origin.y += 44;
rect.size.height -= 44;
[self setFrame:rect];
}
}
#end
This is not a perfect implementation because it uses a hardcoded value for the navigation bar's height, does not take into account a possible toolbar, etc. But all these you can add as properties to this view and in viewDidLoad, before it starts going into the view hierarchy, set the parameters according to your needs.
You can remove the prompt when the user taps the back button, like this
override func willMove(toParentViewController parent: UIViewController?) {
super.willMove(toParentViewController: parent)
if parent == nil {
navigationItem.prompt = nil
}
}
The problem exists whether your nav bars are opaque or translucent. It sucks that Apple has allowed this heinous bug to plague us for over three years now.
All of these solutions are hacks. My solution is to either A) NEVER use prompts, or B) use them in every single view even if you have to set them to "".
You've given the answer yourself - brilliantly. It's a bug, but checking Translucent avoids the bug. Therefore the solution is to check Translucent and then compensate so that the nav bar looks the way you want.
To do so, make a small black rectangle image and include it in your project. Set the background image of the nav bar to this image. Check Translucent. Problem solved! The nav bar is now black opaque in appearance, but it no longer exhibits the bug.
Swift version:
class PromptViewSideEffect: UIView {
override func didMoveToSuperview() {
super.didMoveToSuperview()
if let superview: UIView = self.superview {
let rect: CGRect = superview.bounds
rect.origin.y += 44
rect.size.height -= 44
self.frame = rect
}
}
}
I have been reading a lot about iOS7 UI transition.
I am not able to get what these three properties automaticallyAdjustsScrollViewInsets, extendedLayoutIncludesOpaqueBars, edgesForExtendedLayout??
For example I am trying to make my view controllers start below the status bar but I am not able to achieve it.
Starting in iOS7, the view controllers use full-screen layout by default. At the same time, you have more control over how it lays out its views, and that's done with those properties:
edgesForExtendedLayout
Basically, with this property you set which sides of your view can be extended to cover the whole screen. Imagine that you push a UIViewController into a UINavigationController. When the view of that view controller is laid out, it will start where the navigation bar ends, but this property will set which sides of the view (top, left, bottom, right) can be extended to fill the whole screen.
Let see it with an example:
UIViewController *viewController = [[UIViewController alloc] init];
viewController.view.backgroundColor = [UIColor redColor];
UINavigationController *mainNavigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
Here you are not setting the value of edgesForExtendedLayout, therefore the default value is taken (UIRectEdgeAll), so the view extends its layout to fill the whole screen.
This is the result:
As you can see, the red background extends behind the navigation bar and the status bar.
Now, you are going to set that value to UIRectEdgeNone, so you are telling the view controller to not extend the view to cover the screen:
UIViewController *viewController = [[UIViewController alloc] init];
viewController.view.backgroundColor = [UIColor redColor];
viewController.edgesForExtendedLayout = UIRectEdgeNone;
UINavigationController *mainNavigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
And the result:
automaticallyAdjustsScrollViewInsets
This property is used when your view is a UIScrollView or similar, like a UITableView. You want your table to start where the navigation bar ends, because you wont see the whole content if not, but at the same time you want your table to cover the whole screen when scrolling. In that case, setting edgesForExtendedLayout to None won't work because your table will start scrolling where the navigation bar ends and it wont go behind it.
Here is where this property comes in handy, if you let the view controller automatically adjust the insets (setting this property to YES, also the default value) it will add insets to the top of the table, so the table will start where the navigation bar ends, but the scroll will cover the whole screen.
This is when is set to NO:
And YES (by default):
In both cases, the table scrolls behind the navigation bar, but in the second case (YES), it will start from below the navigation bar.
extendedLayoutIncludesOpaqueBars
This value is just an addition to the previous ones. By default, this parameter is set to NO. If the status bar is opaque, the views won't be extended to include the status bar, even if you extend your view to cover it (edgesForExtendedLayout to UIRectEdgeAll).
If you set the value to YES, this will allow the view to go underneath the status bar again.
If something is not clear, write a comment and I'll answer it.
How does iOS know what UIScrollView to use?
iOS grabs the first subview in your ViewController's view, the one at index 0, and if it's a subclass of UIScrollView then applies the explained properties to it.
Of course, this means that UITableViewController works by default (since the UITableView is the first view).
Not sure if you are using storyboards, but if you are, to make your view controllers start below the status bar (and above the bottom bar):
Select the view controller in IB,
In the attributes inspector, deselect 'Extend Edges - Under Top Bars' and 'Extend Edges - Under Bottom Bars'.
I am using storyboards and using the above advice worked however I wasn't exactly sure how to implement it. Below is a short example in swift of how it cleared up the problem by putting the recommended solution into the ViewController.
import Foundation
import UIKit
// This ViewController is connected to a view on a storyboard that
// has a scrolling sub view.
class TheViewController: UIViewController {
// Prepares the view prior to loading. Putting it in viewDidAppear didn't work.
override func viewWillAppear(animated: Bool) {
// this method is an extension of the UIViewController
// so using self works as you might expect.
self.automaticallyAdjustsScrollViewInsets = false
// Default is "true" so this sets it to false tells it to use
// the storyboard as you have it placed
// and not how it thinks it should place it.
}
}
My Problem:
Auto Adjust set to true by default causing a difference between storyboard design and simulator
Resolved:
Code above applied, turning off the auto-adjust.
I solved this problem by adding this line, but my problem was related to a UIView, not UIScrollView
self.navigationController.navigationBar.translucent = NO;
Just bare in mind that
automaticallyAdjustsScrollViewInsets
property works only if some kind of scroll view (table view, collection view,...) is either
The view of VC, or
First subview of this view
Other suggested, that it doest work even if it is the first subview, but there are other scroll views in the view hierarchy.
EDIT (extension DIY)
If you want similar behaviour even if you can't fulfil these conditions (e.g. you have a background image below the scroll view), you can adjust the scroll view insets manually. But please don't set it to constant like 44 or 64 or even 20 like many suggest around SO. You can't know the size ever. There can be the incall/gps/audio notification, navigation bar doesn't have to be always 44 pts etc.
I think the best solution is to use layoutGuide length in didLayoutSubviews:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentInset = UIEdgeInsets(top: topLayoutGuide.length, left: 0, bottom: 0, right: 0)
scrollView.scrollIndicatorInsets = scrollView.contentInset
}
You can use the bottomLayoutGuide in the same way.
This is a weird error that may just be an issue in Xcode for all I know. I have a tab bar controller where the first view is a UITableView with (obviously) a number of cells. When you select a cell, I've set up a segue on the MainStoryboard to go to a detail view controller. I want the tab bar to be hidden when I go to the detail view, so I went into the storyboard, chose my detail view, and clicked "Hides Bottom Bar on Push" in the editor screen that starts with "Simulated Metrics."
Everything works just fine, except that when I tap on a cell, a black bar flashes at the top of the UITableView screen, dropping the tableview cells down (as if the cells are falling down below the tab bar at the bottom), just before the screen pushes over to the detail view. The effect isn't harmful at all, but it's very disconcerting, and I'd like to smooth that out.
The only fix I've found is to uncheck the "Hides Bottom Bar when Pushed" option on the storyboard. That indeed does get rid of that black bar flash, but of course the tab bar stays on the screen when I go to the detail view, which is what I don't want.
Any ideas?
Just for completeness' sake, I went ahead and ran
[self.navigationController setToolbarHidden:YES animated: YES];
on the detail view controller's viewWillAppear method (and even tried it with the storyboard option both on and off), but there was no difference. The toolbar did indeed hide just fine, but I still got that black line at the top. So weird.
I know it is too late !!! I ran into same issue. It seems like the Auto resizing mask for the view was incorrect to be exact the UIViewAutoresizingFlexibleTopMargin. I checked this on in the xib file. If you are trying to do it in code make sure this flag -UIViewAutoresizingFlexibleTopMargin - is not included in the autoresizing mask.
Hope this will help some one in the future
I know it is a bit late, but I have same problem and I can't solve it with any of the previous answers. (I suppose this is the reason non was accepted).
The problem is that view size of the SecondViewController is same as view size of a previous ViewController, so too small to fit in a ViewController with Toolbar hidden. Thats why black background of a UITabBarController is visible at the top when transition is happening, and on a viewDidAppear view will stretch on right size.
For me it help to subclass root UITabBarController and set background color to same background color as SecondViewController has.
class RootViewController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = Style.backgroundColor
}
}
Then you can leave checkbox checked inside storyboard and it will look ok.
P.S.
If you have some views, that is position on the bottom part of the view, you need to set bottom constraints so they are smaller by 49 (because this is the height of the toolbar), and then on viewDidAppear set the right constraint.
For example:
I have view that need to be position 44 px from bottom edge. Before, I have constraint set to 44 and I have some strange behaviour of that view. It was placed to height and then jump on the right place.
I fix this with setting constraint to -5 (44-49), and then in viewDidAppear set the constraint back to 44. Now I have normal behaviour of that view.
Wow I just had the same issue now, very painful, and no info on the net about it.
Anyway, a simple workaround for me was to change the current view's Frame moving the y coordinates up and making the height bigger by the height of the tab bar. This fixed the problem if done straight after pushing the new view onto the navigation controller. Also, there was no need to fix the Frame afterwards (it must be updated when the view is shown again).
MonoTouch code:
UIViewController viewControllerToPush = new MyViewController();
viewControllerToPush.HidesBottomBarWhenPushed = true; // I had this in the MyViewController's constructor, doesn't make any difference
this.NavigationController.PushViewController(viewControllerToPush, true);
float offset = this.TabBarController.TabBar.Frame.Height;
this.View.Frame = new System.Drawing.RectangleF(0, -offset, this.View.Frame.Width, this.View.Frame.Height + offset);
Objective C code (untested, just a translation of the monotouch code):
UIViewController *viewControllerToPush = [MyViewController new];
viewControllerToPush.hidesBottomBarWhenPushed = YES; viewControllerToPush.hidesBottomBarWhenPushed = YES;
float offset = self.tabBarController.tabBar.frame.size.height; float offset = self.tabBarController.tabBar.frame.size.height;
self.view.frame = CGRectMake(0, -offset, self.view.frame.width, self.view.frame.height + offset); self.view.frame = CGRectMake(0, -offset, self.view.frame.size.width, self.view.frame.size.height + offset);
Do this in viewWillAppear of detailViewController, it should work fine
subclass your navigation controller, or just find the navigation bar
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let backdropEffectView = navigationBar.subviews[0].subviews[0].subviews[0] //_UIBackdropEffectView
let visualEffectView: UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .Light))
visualEffectView.frame = backdropEffectView.frame
backdropEffectView.superview?.insertSubview(visualEffectView, aboveSubview: backdropEffectView)
backdropEffectView.removeFromSuperview()
}