iOS 8 adds a super new cool feature: hiding the navigation bar when user is scrolling.
This with a single line in viewDidload :
navigationController?.hidesBarsOnSwipe = true
Cool, isn't it?
But now I have a little problem: when the navigation bar is hidden, the status bar is still here and overlaps content, which is ugly.
What should I do to make it hidden when the navigation bar is hidden?
Override the following methods on UIViewController:
extension MyViewController {
override func prefersStatusBarHidden() -> Bool {
return barsHidden // this is a custom property
}
// Override only if you want a different animation than the default
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
}
Update barsHidden somewhere in the code and call
setNeedsStatusBarAppearanceUpdate()
This is fixed problem for in Xcode 6.1
navigationController?.navigationBar.hidden = true
I am basing this answer on some comments on this post, which are speculation. I am not sure if this will work, because Apple does not give us any direct way or delegate method for when the navigation bar hides.
Subclass UINavigationBar as NavigationBar. Add a property observer to its hidden property like so:
var hidden: Bool{
didSet{
UIApplication.sharedApplication().setStatusBarHidden(self.hidden, animation: .Slide)
}
}
You want to then go to your viewDidLoad method in your main view controller, and set your self.navigationBar property (or self.navigationController.navigationBar, not sure which one) to an instance of your new NavigationBar class.
Note that I cannot test this right now, let me know how/if this works.
You could detect swipes by using UISwipeGestureRecognizer. I'm using it on UIWebView:
In viewDidLoad I have:
let swipeUp = UISwipeGestureRecognizer(target: self, action: "didSwipe")
let swipeDown = UISwipeGestureRecognizer(target: self, action: "didSwipe")
swipeUp.direction = UISwipeGestureRecognizerDirection.Up
swipeDown.direction = UISwipeGestureRecognizerDirection.Down
webView.addGestureRecognizer(swipeUp)
webView.addGestureRecognizer(swipeDown)
navigationController?.hidesBarsOnSwipe = true
I also have an extension to my viewcontroller, called WebViewViewController:
extension WebViewViewController {
override func prefersStatusBarHidden() -> Bool {
return hideStatusBar
}
override func preferredStatusBarUpdateAnimation() -> UIStatusBarAnimation {
return UIStatusBarAnimation.Slide
}
}
On a class level in my WebViewViewController I also have:
var hideStatusBar = false
func didSwipe() {
hideStatusBar = true
}
Okay I spent all day doing this, hopefully this helps some people out. There's a barHideOnSwipeGestureRecognizer. So you could make a listener for the corresponding UIPanGesture, noting that if the navigation bar is hidden then its y origin is -44.0; otherwise, it's 0 (not 20 because we hid the status bar!).
In your view controller:
// Declare at beginning
var curFramePosition: Double!
var showStatusBar: Bool = true
self.navigationController?.barHideOnSwipeGestureRecognizer.addTarget(self, action: "didSwipe:")
...
override func viewDidLoad(){
self.navigationController?.hidesBarsOnSwipe = true
curFramePosition = 0.0 // Not hidden
self.navigationController?.barHideOnSwipeGestureRecognizer.addTarget(self, action: "didSwipe:")
...
}
func didSwipe(swipe: UIPanGestureRecognizer){
// Visible to hidden
if curFramePosition == 0 && self.navigationController?.navigationBar.frame.origin.y == -44 {
curFramePosition = -44
showStatusBar = false
prefersStatusBarHidden()
setNeedsStatusBarAppearanceUpdate()
}
// Hidden to visible
else if curFramePosition == -44 && self.navigationController?.navigationBar.frame.origin.y == 0 {
curFramePosition = 0
showStatusBar = true
prefersStatusBarHidden()
setNeedsStatusBarAppearanceUpdate()
}
}
override func prefersStatusBarHidden() -> Bool {
if showStatusBar{
return false
}
return true
}
Related
I've been trying this for awhile. The code below is my UIPresentationController. When a button is pressed, I add a dimmed UIView and a second modal (presentedViewController) pops up halfway.
I added the tap gesture recognizer in the method presentationTransitionWillBegin()
I don't know why the tap gesture is not being registered when I click on the dimmed UIView.
I've tried changing the "target" and adding the gesture in a different place. Also looked at other posts, but nothing has worked for me.
Thanks
import UIKit
class PanModalPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
var frame: CGRect = .zero
frame.size = size(forChildContentContainer: presentedViewController, withParentContainerSize: containerView!.bounds.size)
frame.origin.y = containerView!.frame.height * (1.0 / 2.0)
print("frameOfPresentedViewInContainerView")
return frame
}
private lazy var dimView: UIView! = {
print("dimView")
guard let container = containerView else { return nil }
let dimmedView = UIView(frame: container.bounds)
dimmedView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
dimmedView.isUserInteractionEnabled = true
return dimmedView
}()
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
print("init presentation controller")
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override func presentationTransitionWillBegin() {
guard let container = containerView else { return }
print("presentation transition will begin")
container.addSubview(dimView)
dimView.translatesAutoresizingMaskIntoConstraints = false
dimView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
dimView.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
dimView.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
dimView.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
dimView.isUserInteractionEnabled = true
let recognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
dimView.addGestureRecognizer(recognizer)
container.addSubview(presentedViewController.view)
presentedViewController.view.translatesAutoresizingMaskIntoConstraints = false
presentedViewController.view.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
presentedViewController.view.widthAnchor.constraint(equalTo: container.widthAnchor).isActive = true
presentedViewController.view.heightAnchor.constraint(equalTo: container.heightAnchor).isActive = true
guard let coordinator = presentingViewController.transitionCoordinator else { return }
coordinator.animate(alongsideTransition: { _ in
self.dimView.alpha = 1.0
})
print(dimView.alpha)
}
override func dismissalTransitionWillBegin() {
guard let coordinator = presentedViewController.transitionCoordinator else {
print("dismissal coordinator")
self.dimView.alpha = 0.0
return
}
print("dismissal transition begin")
coordinator.animate(alongsideTransition: { _ in
self.dimView.alpha = 0.0
})
}
override func containerViewDidLayoutSubviews() {
print("containerViewDidLayoutSubviews")
presentedView?.frame = frameOfPresentedViewInContainerView
// presentedViewController.dismiss(animated: true, completion: nil)
}
override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize {
print("size")
return CGSize(width: parentSize.width, height: parentSize.height * (1.0 / 2.0))
}
#objc func handleTap(_ sender: UITapGestureRecognizer) {
print("tapped")
// presentingViewController.dismiss(animated: true, completion: nil)
presentedViewController.dismiss(animated: true, completion: nil)
}
}
I can't tell what the frame/bounds of your presentedViewController.view is but even if it's top half has an alpha of 0 it could be covering your dimView and receiving the tap events instead of the dimView - since presentedViewController.view is added as a subview on top of dimView.
You may have to wait until after the controller is presented and add the gesture to its superview's first subview. I've used this before to dismiss a custom alert controller with a background tap. You could probably do something similar:
viewController.present(alertController, animated: true) {
// Enabling Interaction for Transparent Full Screen Overlay
alertController.view.superview?.subviews.first?.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: alertController, action: #selector(alertController.dismissSelf))
alertController.view.superview?.subviews.first?.addGestureRecognizer(tapGesture)
}
Hmm, try using this instead. Let me know how it goes. It works for me.
class PC: UIPresentationController {
/*
We'll have a dimming view behind.
We want to be able to tap anywhere on the dimming view to do a dismissal.
*/
override var frameOfPresentedViewInContainerView: CGRect {
let f = super.frameOfPresentedViewInContainerView
var new = f
new.size.height /= 2
new.origin.y = f.midY
return new
}
override func presentationTransitionWillBegin() {
let con = self.containerView!
let v = UIView(frame: con.bounds)
v.backgroundColor = UIColor.black
v.alpha = 0
con.insertSubview(v, at: 0)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
v.addGestureRecognizer(tap)
let tc = self.presentedViewController.transitionCoordinator!
tc.animate(alongsideTransition: { _ in
v.alpha = 1
}, completion: nil)
}
#objc func handleTap() {
print("tapped")
self.presentedViewController.dismiss(animated: true, completion: nil)
}
override func dismissalTransitionWillBegin() {
let con = self.containerView!
let v = con.subviews[0]
let tc = self.presentedViewController.transitionCoordinator!
tc.animate(alongsideTransition: { _ in
v.alpha = 0
}, completion: nil)
}
}
I took a look at your project just now. The problem is in your animation controller. If you comment out the functions in your transition delegate object that vend animation controllers, everything works fine.
But just looking at your animation controller, what you wanted to achieve was to have your new vc slide up / slide down. And in fact, you don't even need a custom animation controller for this; the modalTransitionStyle property of a view controller has a default value of coverVertical, which is just what you want I think.
In any case though, you can still use the presentation controller class I posted before, as it has same semantics from your class, just without unnecessary overrides.
Optional
Also just a tip if you'd like, you have these files right now in your project:
PanModalPresentationDelegate.swift
PanModalPresentationController.swift
PanModalPresentationAnimator.swift
TaskViewController.swift
HomeViewController.swift
What I normally do is abbreviate some of those long phrases, so that the name of the file and class conveys the essence of its nature without long un-needed boilerplate.
So HomeViewController and TaskViewController would be Home_VC and Task_VC. Those other 3 files are all for the presentation of one VC; it can get out of hand very quickly. So what I normally do there is call my presentation controller just PC and nest its declaration inside the VC class that will use it (in this case that's Task_VC). Until the time comes where it needs to be used by some other VC too; then it's more appropriate to put it in its own file and call it Something_PC but I've never actually needed to do that yet lol. And the same for any animation controllers ex. Fade_AC, Slide_AC etc. I tend to call transition delegate a TransitionManager and nest it in the presented VC's class. Makes it easier for me to think of it as just a thing that vends AC's / a PC.
Then your project simply becomes:
Home_VC.swift
Task_VC.swift
And if you go inside Task_VC, you'll see a nested TransitionManager and PC.
But yeah up to you 😃.
The dimmedView is behind presented view. You have a couple options to correct that.
First, is allow touches to pass through the top view, it must override pointInside:
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView *subview in self.subviews) {
if ([subview hitTest:[self convertPoint:point toView:subview] withEvent:event]) {
return TRUE;
}
}
return FALSE;
}
Another options is to instead add the gesture recognizer to the presentedViewController.view, instead of the dimmedView. And, if you allow PanModalPresentationController to adopt the UIGestureRecognizerDelegate, and it as the delegate to the recognizer, you can determine if you should respond to touches, by implementing shouldReceive touch:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if (touch.view == presentedViewController.view) {
return true
}
return false
}
If you use the second option, don't forget to remove the gesture recognizer in dismissalTransitionWillBegin or dismissalTransitionDidEnd!
For example, on my current VC, I am not showing the status bar but when I modally present another VC that shows the status bar, the current one does a shift animation which looks choppy. How could I go about not messing with the current VC and having the modally presented one fade in the status bar as it slides up?
Add a flag that determines if status bar is hidden
var statusBarHidden = false
Override prefersStatusBarHidden
override func prefersStatusBarHidden() -> Bool {
return statusBarHidden
}
Add utility function to update status bar visibility with animation
func setStatusBar(hidden: Bool) {
statusBarHidden = hidden
UIView.animate(withDuration: 0.25, animations: {
self.setNeedsStatusBarAppearanceUpdate()
}) { (success: Bool) in
print("status bar animated to hidden: \(statusBarHidden)")
}
}
var hideStatusBar = false
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
override var prefersStatusBarHidden: Bool {
return hideStatusBar
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
hideStatusBar = true
setNeedsStatusBarAppearanceUpdate()
}
This code will slide the the status bar up smoothly once your view appears. You can also try a different animation by replacing:
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
with:
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .fade
}
Use self.prefersStatusBarHidden() depending on Particular View
override func prefersStatusBarHidden() -> Bool {
return true
}
I tried calling tabBarController!.tabBar.hidden = true in viewDidLoad() and it hides the TabBar. However, I tried to set tap gesture and hide the bar on Tap. The parent viewController that has ScrollView inside it with subview (that is connected with IBOutlet as myView)
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: Selector("handleTap:"))
myView.addGestureRecognizer(tap)
}
func handleTap(sender: UITapGestureRecognizer? = nil) {
print("A") // logs successfully
if TabBarHidden == false {
print("B") // logs successfully
//I tried:
tabBarController?.tabBar.hidden = true
// I also tried
tabBarController?.tabBar.alpha = 0
tabBarController?.tabBar.frame.origin.x += 50
hidesBottomBarWhenPushed = true
} else {
...
TabBarHidden = false
}
}
hidden does work when I call it in viewDidLoad as I said, but not if I call in tap gesture function. What may be the problem? What am I missing?
this code totally works for me:
class ViewController: UIViewController {
var tabBarHidden: Bool = false {
didSet {
tabBarController?.tabBar.hidden = tabBarHidden
}
}
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized(_:)))
view.addGestureRecognizer(tapGestureRecognizer)
}
func tapGestureRecognized(sender: UITapGestureRecognizer) {
tabBarHidden = !tabBarHidden
}
}
The Problem
I have a problem with my split view. It works fine on iPhone and iPad simulators, but on the iPhone 6+ I lose the navigation bar after rotating the device. Here's what happens on the 6+ simulator:
I start the app and it presents a + button in the navigation bar. I tap this button.
It loads a view controller over the existing view. A navigation bar, as expected, is visible with a working back button.
I turn the device horizontally. As intended the new controller appears in the Master section, with an empty detail section on the right. Unfortunately the navigation bar dissapears.
When I turn the device vertically the navigation bar does not reappear.
In fact when I turn the device horizontally it seems the navigation controller is removed from the stack (I've observed this from outputting the contents of splitViewContoller.viewControllers).
My Code
The test application is simply the Master Detail template with a few modifications.
I've added a new "Add Item" controller and then created a show segue from the Master view's "+" button. The "Add Item" controller is blank, just a blue background.
The DetailViewController has a timerStarted boolean value that is true when the detail view is being used and false when it isn't. The master view is hidden when the detail is in use and displayed when it isn't.
Here's the relevant code (there's nothing interesting in AppDelegate as it's no longer a split view delegate, and MasterViewController has no interaction as the button works via the storyboard)
DetailViewController
import UIKit
class DetailViewController: UIViewController, UISplitViewControllerDelegate {
#IBOutlet weak var detailDescriptionLabel: UILabel!
var collapseDetailViewController = true
var detailItem: AnyObject? {
didSet {
self.configureView()
}
}
var timerStarted: Bool = false {
didSet {
self.changeTimerStatus()
}
}
func configureView() {
if let detail: AnyObject = self.detailItem {
if let label = self.detailDescriptionLabel {
label.text = detail.description
self.timerStarted = true
}
}
}
func changeTimerStatus() {
if self.timerStarted {
if splitViewController!.collapsed == false {
UIView.animateWithDuration(0.3, animations: {
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
})
}
collapseDetailViewController = false
} else {
if splitViewController!.collapsed {
self.splitViewController?.viewControllers[0].popToRootViewControllerAnimated(true)
} else {
UIView.animateWithDuration(0.3, animations: {
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible
})
}
collapseDetailViewController = true
}
}
override func viewDidLoad() {
super.viewDidLoad()
splitViewController?.delegate = self
self.disabledScreen.hidden = false
self.view.bringSubviewToFront(disabledScreen)
self.configureView()
}
override func viewWillAppear(animated: Bool) {
if splitViewController!.collapsed == false && self.timerStarted == false {
splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible
}
}
#IBAction func closeButton(sender: AnyObject) {
self.timerStarted = false
}
func primaryViewControllerForExpandingSplitViewController(splitViewController: UISplitViewController) -> UIViewController? {
if timerStarted == true {
splitViewController.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
} else {
splitViewController.preferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible
}
return nil
}
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController!, ontoPrimaryViewController primaryViewController: UIViewController!) -> Bool {
return collapseDetailViewController
}
}
AddItemViewController
import UIKit
class AddItemViewController: UIViewController, UISplitViewControllerDelegate {
var collapseDetailViewController = false
override func viewDidLoad() {
super.viewDidLoad()
self.splitViewController?.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewDidAppear(animated: Bool) {
self.splitViewController?.delegate = self
self.collapseDetailViewController = false
}
override func viewWillDisappear(animated: Bool) {
self.splitViewController?.delegate = nil
self.collapseDetailViewController = true
}
func primaryViewControllerForExpandingSplitViewController(splitViewController: UISplitViewController) -> UIViewController? {
return self
}
func primaryViewControllerForCollapsingSplitViewController(splitViewController: UISplitViewController) -> UIViewController? {
return nil
}
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController!, ontoPrimaryViewController primaryViewController: UIViewController!) -> Bool {
return collapseDetailViewController
}
}
I'd be grateful for any suggestions.
I have found the answer. I read a article which I had originally missed because it focuses on changing the detail view rather than the master. As it turns out, the split view works better if I just manage the detail and then the master will take care of itself. Since I never want to change the detail I can simply add the following to my split view delegate:
func splitViewController(splitViewController: UISplitViewController, separateSecondaryViewControllerFromPrimaryViewController primaryViewController: UIViewController!) -> UIViewController? {
return (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("detailView") as! UIViewController)
}
Once this is done, I no longer lose the navigation bar.
I would like to set the background color of a view to black when the navigation bar is hidden, and to white when the navigation bar is displayed.
The property hidesBarsOnTap is set to true in viewDidLoad. This works fine:
navigationController?.hidesBarsOnTap = true
How can I be notified when the bars are hidden and displayed?
Sorry, I made a mistake. The following code does exactly what you want. If you have a toolbar, you can set it to hide as well.
class ViewController: UIViewController {
var hidden = false {
didSet {
if let nav = navigationController {
nav.setNavigationBarHidden(hidden, animated: true)
nav.setToolbarHidden(hidden, animated: true)
view.backgroundColor = hidden ? UIColor.blackColor() : UIColor.whiteColor()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
let recognizer = UITapGestureRecognizer(target: self, action: "tap:")
view.addGestureRecognizer(recognizer)
}
func tap(recognizer: UITapGestureRecognizer) {
if recognizer.state == .Ended {
hidden = !hidden
}
}
}
since hidesBarsOnTap is of type boolean, we can easily use it to check and use it as option like in below example:
var set : Bool = navigationController?.hidesBarsOnTap //true or false
if (set){
//do what you want when set
}else{
//do what you want when it is not set
}