I have a custom transition for presenting a view controller and I use a UIPresentationController subclass to perform the presentation.
In the presentation controller I add a couple of subviews to its containerView (the chrome). I would like to constrain one of them with something like the topLayoutGuide in order to account for the height of any top bars (like the status bar).
However, UIPresentationController is not a UIViewController subclass and as such it doesn't have a topLayoutGuide property. I tried constraining the views with the presentingViewController's and the presentedViewController's topLayoutGuide but the app crashes because they are not part of the same view hierarchy.
So is there any way to position subviews inside the presentation controller's containerView at its top while still accounting for the height of any top bars?
By trial and error I found out that for topLayoutGuide to work it is important that the added view controllers view height is not larger than the screen size height. In UIViewControllerAnimatedTransitioning in func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
set:
toVC.view.frame = transitionContext.finalFrame(for: toVC)
Related
I have the issue that the topLayoutGuide.length in a UIViewController (from XIB) gets set after viewWillAppear and i don't know how to hook into the change of topLayoutGuide.length to initially set the contentOffset of a table view.
Code to modally present a UIViewController inside a UINavigationController:
let viewController = UIViewController(nibName: "ViewController", bundle: nil)
let navigationController = UINavigationController(rootViewController: viewController)
present(navigationController, animated: true, completion: nil)
My debugging output about the topLayoutGuide.length
Init view controller
-[UIViewController topLayoutGuide]: guide not available before the view controller's view is loaded
willMove toParentViewController - top layout guide nan
Init navigation controller and pass view controller as root vc
Present navigation controller modally
viewDidLoad - top layout guide 0.0
viewWillAppear - top layout guide 0.0
viewWillLayoutSubviews - top layout guide 64.0
viewDidLayoutSubviews - top layout guide 64.0
viewWillLayoutSubviews - top layout guide 64.0
viewDidLayoutSubviews - top layout guide 64.0
viewDidAppear - top layout guide 64.0
didMove toParentViewController - top layout guide 64.0
viewWillLayoutSubviews - top layout guide 64.0
viewDidLayoutSubviews - top layout guide 64.0
For now i use a bool flag in the view controller to set the contentoffset in the viewDidLayoutSubviews only once, even though the method is called multiple times.
Any more elegant solution in mind?
The documentation for the topLayoutGuide states explicitly:
Query this property within your implementation of the viewDidLayoutSubviews() method.
Judging from your own inspections the earliest point to obtain the topLayoutGuide's actual length is inside the viewWillLayoutSubviews() method. However, I would not rely on that and do it in viewDidLayoutSubviews() as the docs suggest.
The reason why you cannot access the property earlier...
... is that the layout guides are objects that depend on the layout of any container view controllers. The views are laid out lazily when they are needed on screen. So when you add the viewController to the navigationViewController as its root view controller it's not laid out yet.
The layout happens when you present the navigationController. At that point the views of both view controllers are loaded (→ viewDidLoad(), viewWillAppear()) and then a layout pass is triggered. First, the navigationViewController's view is laid out (layout flow: superview → subview). The navigation bar's frame is set to a height of 64 px. Now the viewController's topLayoutGuide can be set. And finally the viewController's view is laid out (→ viewWillLayoutSubviews(), viewDidLayoutSubviews()).
Conclusion:
The only way to do some initial layout tweaks that depend on the layout guide's length is the method you suggested yourself:
Have a boolean property in your view controller that you set to true initially:
var isInitialLayoutPass: Bool = true
Inside viewDidLayoutSubviews() check for that property and only perform your initial layout when it's true:
func viewDidLayoutSubviews() {
if isInitialLayoutPass {
tableView.contentOffset = CGPoint(x: 0, y: topLayoutGuide.length)
}
}
Inside viewDidAppear(), set the property to false to indicate that the initial layout is done:
override func viewDidAppear() {
super.viewDidAppear()
isInitialLayoutPass = false
}
I know it still feels a little hacky but I'm afraid it's the only way to go (that I can think of) unless you want to use key-value-observing (KVO) which doesn't make it much neater in my opinion.
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 want to give a UIViewController's view a size that is different from the device's screen size. I know I can usually achieve this by adding the view controller as a child view controller of another parent UIViewController that has defined a frame for the child, but I am in a situation that seems a little different.
I have a UIWindow that only takes up a portion of the screen (it's got a frame that's basically (0, 0, DEVICE_WIDTH, HEIGHT_LESS_THAN_DEVICE_HEIGHT). This window shows up with the proper sizing and positioning when presented. I am setting a view controller as the rootViewController of the window, and then presenting the window by setting its hidden value to false. When this happens, the view controller's view ends up sized to fill the device's screen (i.e. a frame of (0, 0, DEVICE_WIDTH, DEVICE_HEIGHT)).
What I would like is for the view controller to inherit its sizing from the UIWindow it is set as the root view controller of. Is there a way to do this?
I have also tried overriding loadView() and returning a custom-sized view there. Logging the view shows that the view controller's view object is correctly sized during viewDidLoad, but is overwritten with the default size by viewWillAppear:. I would be open to using loadView() to size the view controller if inheriting sizing from the window isn't possible, but I don't know how to make the custom size stick.
Note: The reason why I am trying to add a view controller to the window is because I want to take advantage of the view controller lifecycle methods such as viewDidAppear:, which is why I am not just creating a simple UIView and adding it as a subview of the window.
As counter intuitive as it may seem, if you set set self.view.frame on viewWillAppear (IOS 8) or viewDidAppear (IOS 7) you will be able to make it work.
Swift code (IOS 8):
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Banner style size, for example
self.view.frame = CGRectMake(0, 0, 320, 50)
}
For IOS 7, I had to use viewDidAppear, which is obviously an unsatisfactory solution. So I had to start the view with alpha = 0.0 and set alpha = 1.0 on viewDidAppear, after modifying self.view.frame.
Hope it helps.
I got a view controller hierarchy with the following parent-child relationship:
UINavigationController (contains) MainViewController (contains) UIPageViewController (contains) UITableViewController
In the inner-most UITableViewController, I have automaticallyAdjustScrollViewInset set to YES, however this setting doesn't seem to work. As can be seen in the screenshot below, the table view's contentInset doesn't seem to be adjusted with the navigation bar.
My goal is to have this table view's contentInset to be automatically adjusted with the outer-most navigation bar. If the navigation bar or the status bar is hidden, I want the content inset to adjust accordingly.
How can I do that? Thanks!
The UINavigationController adjust its child view controller's topLayoutGuide automatically.
In your case it is not being propagated down far enough.
If you make sure this topLayoutGuide makes it down to your table view controller then you won't have to set the contentInset manually.
Also, your view controller hierarchy seems overly complex (I don't know the details of your project). This simplified version of what you have will give you what you are after for free.
Apparently the outer-most UINavigationController is still accessible to the inner-most UITableViewController via its navigationController property.
Because the navigation bar's frame is accessible by the inner-most view controller, we can do something like this in its viewDidLayoutSubviews.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.tableView.contentInset = UIEdgeInsetsMake(CGRectGetMaxY(self.navigationController.navigationBar.frame), 0, 0, 0);
self.tableView.scrollIndicatorInsets = self.tableView.contentInset;
}
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.